mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-29 22:11:02 +00:00
Compare commits
320 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a316de3957 | ||
|
|
c3f5a820e4 | ||
|
|
daa5ec7bed | ||
|
|
de91f1f3fa | ||
|
|
9c67a7e357 | ||
|
|
f70ce39fb7 | ||
|
|
20ffe2273c | ||
|
|
f333051073 | ||
|
|
a58f8fa76b | ||
|
|
625c7d738b | ||
|
|
563e4f2cbe | ||
|
|
627bed2407 | ||
|
|
8313d639d7 | ||
|
|
c603de70e3 | ||
|
|
4508cada0f | ||
|
|
ab8dc2ee8b | ||
|
|
acf2dd9a8a | ||
|
|
f562e7d7cf | ||
|
|
25372d5251 | ||
|
|
efb346d0a8 | ||
|
|
6d05fb4413 | ||
|
|
d67a51791e | ||
|
|
332857b2c9 | ||
|
|
a0018b5fb6 | ||
|
|
734c5d0571 | ||
|
|
8e93f66ba8 | ||
|
|
5d8cf8a605 | ||
|
|
87c2d82462 | ||
|
|
45a341397b | ||
|
|
b018d0f090 | ||
|
|
3c992f89f4 | ||
|
|
8067e0d0ac | ||
|
|
4cee512572 | ||
|
|
87a9df4c12 | ||
|
|
ea96291bfc | ||
|
|
b1eedce229 | ||
|
|
0d32342765 | ||
|
|
d81391f593 | ||
|
|
3bd9dc031a | ||
|
|
4f07421df7 | ||
|
|
8eadd20968 | ||
|
|
c6d04d99b3 | ||
|
|
91b1f4775b | ||
|
|
5bd8f1a3c7 | ||
|
|
39fc508cfe | ||
|
|
664b5a4bdd | ||
|
|
ff02280239 | ||
|
|
26b6564825 | ||
|
|
5459908201 | ||
|
|
3693179c78 | ||
|
|
9416c88511 | ||
|
|
f18399d529 | ||
|
|
6b2ffdaf4f | ||
|
|
d16dd7ed67 | ||
|
|
8142c966c0 | ||
|
|
b7cc35207c | ||
|
|
e398238fe6 | ||
|
|
51a5609395 | ||
|
|
4e6842862e | ||
|
|
ddde08c61b | ||
|
|
05b3f57a76 | ||
|
|
0464cc08c3 | ||
|
|
4be3ded9c8 | ||
|
|
6a42832855 | ||
|
|
84fc5e6e2c | ||
|
|
8375cb5c03 | ||
|
|
2fdee06248 | ||
|
|
f861b3621f | ||
|
|
0cfcfcb9ac | ||
|
|
68ccff2259 | ||
|
|
aa972c916a | ||
|
|
b0673d4f78 | ||
|
|
5170288050 | ||
|
|
61150066bd | ||
|
|
bd6197031a | ||
|
|
98cb11e841 | ||
|
|
52dadf34cf | ||
|
|
f038dcb255 | ||
|
|
a851c0f715 | ||
|
|
a0b6956ca4 | ||
|
|
2f41515b33 | ||
|
|
063d314c36 | ||
|
|
e7631e021e | ||
|
|
e65fa8d565 | ||
|
|
14d08b9491 | ||
|
|
cc5b512441 | ||
|
|
84e300482a | ||
|
|
46b84ffc76 | ||
|
|
ad1979505e | ||
|
|
310f068e79 | ||
|
|
431617e6b5 | ||
|
|
33bb60baad | ||
|
|
e847ec21c3 | ||
|
|
e0a1f6534f | ||
|
|
1ba67280a6 | ||
|
|
419d33a3ac | ||
|
|
f12a4de04b | ||
|
|
3077f39c9d | ||
|
|
97cd3dd43b | ||
|
|
038b8f7ff7 | ||
|
|
3d3c9feaec | ||
|
|
7e5def3a37 | ||
|
|
e3e3965795 | ||
|
|
158ea60047 | ||
|
|
2e13d79615 | ||
|
|
f5297f4927 | ||
|
|
326b848e57 | ||
|
|
01f9e86475 | ||
|
|
af992bd19c | ||
|
|
51b3aac0c0 | ||
|
|
8df2107ef9 | ||
|
|
4286232d17 | ||
|
|
ef30869b62 | ||
|
|
ae8b952b4c | ||
|
|
486be4827e | ||
|
|
98a3a1107b | ||
|
|
7228817c68 | ||
|
|
7dbf951d5a | ||
|
|
3ff492d94c | ||
|
|
7fae64bee9 | ||
|
|
d16fbd9a43 | ||
|
|
41830dba4d | ||
|
|
5561c003cf | ||
|
|
62b1a3b900 | ||
|
|
c9649751d2 | ||
|
|
bbc986784b | ||
|
|
7684a15e94 | ||
|
|
42c3b42c05 | ||
|
|
a8711241a7 | ||
|
|
549d7f9db3 | ||
|
|
e83a580486 | ||
|
|
bf908c5e37 | ||
|
|
ebabff4667 | ||
|
|
c352222e3a | ||
|
|
d177087ae6 | ||
|
|
38c5ae447a | ||
|
|
eb75d299d2 | ||
|
|
5339593e17 | ||
|
|
0bacfb8494 | ||
|
|
7ebb539bba | ||
|
|
d7c6d63d71 | ||
|
|
11d04ecb58 | ||
|
|
74328cf4cf | ||
|
|
cfd59a6ba0 | ||
|
|
1779276154 | ||
|
|
dfc10d5520 | ||
|
|
f090f6c630 | ||
|
|
a13f98f6da | ||
|
|
cc98e2f307 | ||
|
|
5c4e9d7696 | ||
|
|
b180625636 | ||
|
|
31482674c0 | ||
|
|
c7bc6241dc | ||
|
|
86b74f022b | ||
|
|
7336c73561 | ||
|
|
528f70c6de | ||
|
|
2c0d698ac9 | ||
|
|
d404202371 | ||
|
|
ebeffa2135 | ||
|
|
51015dc2f4 | ||
|
|
b840cdb695 | ||
|
|
e6cb10df19 | ||
|
|
1cd1b8af23 | ||
|
|
2b38869c41 | ||
|
|
6c310713d6 | ||
|
|
0a2ecdd190 | ||
|
|
3db4363100 | ||
|
|
713960e247 | ||
|
|
b6be7075b0 | ||
|
|
82bc215da5 | ||
|
|
e8f3d5525d | ||
|
|
d1cf8c4e10 | ||
|
|
f19e112d0a | ||
|
|
9eb29361dc | ||
|
|
133959a34e | ||
|
|
bd48ff05eb | ||
|
|
1d2ce6ccaa | ||
|
|
3a3857e9eb | ||
|
|
38c4440d45 | ||
|
|
85f03ece85 | ||
|
|
e2f02dc93c | ||
|
|
88c4d1f8a7 | ||
|
|
2c24a56446 | ||
|
|
d11b370415 | ||
|
|
f81c566f12 | ||
|
|
9a4ed7ad54 | ||
|
|
07793b11d6 | ||
|
|
aad3c3fed3 | ||
|
|
f79bd9194a | ||
|
|
5ad68f2bd2 | ||
|
|
f463275a73 | ||
|
|
ac6b22f659 | ||
|
|
ac9d3a2363 | ||
|
|
38a27c45a1 | ||
|
|
33bfbd65fb | ||
|
|
ac98417355 | ||
|
|
876304065d | ||
|
|
9fc80d6397 | ||
|
|
8797af0cbc | ||
|
|
97a4cba680 | ||
|
|
acc5069c83 | ||
|
|
fab978dba4 | ||
|
|
ad1734d640 | ||
|
|
d687911c85 | ||
|
|
1d4257b1b3 | ||
|
|
55521ab9fc | ||
|
|
17e53a54af | ||
|
|
dc1edc9a42 | ||
|
|
b8782b0507 | ||
|
|
0d422a57e7 | ||
|
|
1bbc98d350 | ||
|
|
7ae6831628 | ||
|
|
65e89398d9 | ||
|
|
2b77b7578c | ||
|
|
e77ab2800a | ||
|
|
c1a0eeb361 | ||
|
|
393ab1e513 | ||
|
|
e26a6c647f | ||
|
|
ea83b722a6 | ||
|
|
34a3e9e5a3 | ||
|
|
7f92ac686d | ||
|
|
b6c79dae40 | ||
|
|
8c957007ab | ||
|
|
c728eae2ba | ||
|
|
3ded6ba87a | ||
|
|
111fb16266 | ||
|
|
121be4bc6f | ||
|
|
afa960c808 | ||
|
|
1df528c0dc | ||
|
|
f792296f78 | ||
|
|
d512929387 | ||
|
|
c7bc1ffe9e | ||
|
|
32f918450a | ||
|
|
f01377f0b1 | ||
|
|
c2a07278fc | ||
|
|
0a17bed243 | ||
|
|
c5ed8acfa3 | ||
|
|
e5f2bb6566 | ||
|
|
b4093b0c47 | ||
|
|
d0fd62abf2 | ||
|
|
4d0c3e5849 | ||
|
|
d131562f34 | ||
|
|
cf2d9ad654 | ||
|
|
af326c8258 | ||
|
|
ba351df331 | ||
|
|
d4c2df37ae | ||
|
|
79d1c44e63 | ||
|
|
38faedb4b5 | ||
|
|
39b0f28127 | ||
|
|
a1913ed968 | ||
|
|
f7917df907 | ||
|
|
84c58fbe6c | ||
|
|
75895d851f | ||
|
|
6d05a42168 | ||
|
|
594fa4daa9 | ||
|
|
8d9254140d | ||
|
|
533aa9f56e | ||
|
|
c310bea0e9 | ||
|
|
813f7a0992 | ||
|
|
4c0f56d3e3 | ||
|
|
1f44d32f35 | ||
|
|
d937f447ef | ||
|
|
187262a266 | ||
|
|
f40ebc9d09 | ||
|
|
3998d88297 | ||
|
|
4db301ca7a | ||
|
|
3dfcc9fc31 | ||
|
|
df63586c02 | ||
|
|
d7372d4dbb | ||
|
|
f4266d0da3 | ||
|
|
736b06bdbe | ||
|
|
5543d29317 | ||
|
|
2fc351f57a | ||
|
|
eee1242964 | ||
|
|
a58e9a523a | ||
|
|
cd3aad1c33 | ||
|
|
5a482d8307 | ||
|
|
91d869005c | ||
|
|
05e73269d3 | ||
|
|
3dac48ced8 | ||
|
|
8c5726ab8a | ||
|
|
076516be23 | ||
|
|
1059a3c17e | ||
|
|
c75df942f2 | ||
|
|
cfe7be5cdb | ||
|
|
390fc18c4c | ||
|
|
4c82c56828 | ||
|
|
da5c480ba7 | ||
|
|
20acd71b1a | ||
|
|
231c9c5b98 | ||
|
|
4cfdcdb23c | ||
|
|
acb0225699 | ||
|
|
ebffaaa742 | ||
|
|
1760064555 | ||
|
|
44a6db3fc2 | ||
|
|
aab25d157e | ||
|
|
8a4be86ddc | ||
|
|
31baf729be | ||
|
|
b98e3dc780 | ||
|
|
878d58679e | ||
|
|
f500ba6cf0 | ||
|
|
d124736556 | ||
|
|
1a825e2509 | ||
|
|
6e14c2221d | ||
|
|
5b6e351a56 | ||
|
|
c310708401 | ||
|
|
f0093b903a | ||
|
|
d33568f0ad | ||
|
|
26f9f40042 | ||
|
|
7545870f38 | ||
|
|
3368a1bc8d | ||
|
|
9c0ef7a788 | ||
|
|
960c2b4113 | ||
|
|
1eb85d4419 | ||
|
|
20bea76e6c | ||
|
|
866bd3b3a9 | ||
|
|
3567b8dced | ||
|
|
d109914537 | ||
|
|
da4d55a9a8 | ||
|
|
63526c6ed3 |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [rebelonion]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
custom: ['https://www.buymeacoffee.com/rebelonion']
|
||||||
67
.github/workflows/beta.yml
vendored
Normal file
67
.github/workflows/beta.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
name: Build APK and Notify Discord
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths-ignore:
|
||||||
|
- '**/README.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set variables
|
||||||
|
run: |
|
||||||
|
VER=$(grep -E -o "versionName \".*\"" app/build.gradle | sed -e 's/versionName //g' | tr -d '"')
|
||||||
|
SHA=${{ github.sha }}
|
||||||
|
VERSION="$VER.${SHA:0:7}"
|
||||||
|
echo "Version $VERSION"
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: 17
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Decode Keystore File
|
||||||
|
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
|
||||||
|
|
||||||
|
- name: List files in the directory
|
||||||
|
run: ls -l
|
||||||
|
|
||||||
|
- name: Make gradlew executable
|
||||||
|
run: chmod +x ./gradlew
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew assembleDebug -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload a Build Artifact
|
||||||
|
uses: actions/upload-artifact@v3.0.0
|
||||||
|
with:
|
||||||
|
name: Dantotsu
|
||||||
|
path: "app/build/outputs/apk/debug/app-debug.apk"
|
||||||
|
|
||||||
|
- name: Upload APK to Discord
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" )
|
||||||
|
curl -F "payload_json={\"content\":\" Debug-Build: <@719439449423085569> **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
|
- name: Delete Old Pre-Releases
|
||||||
|
id: delete-pre-releases
|
||||||
|
uses: sgpublic/delete-release-action@master
|
||||||
|
with:
|
||||||
|
pre-release-drop: true
|
||||||
|
pre-release-keep-count: 3
|
||||||
|
pre-release-drop-tag: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -23,8 +23,8 @@ output.json
|
|||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
|
|
||||||
# Google Services (e.g. APIs or Firebase)
|
|
||||||
google-services.json
|
|
||||||
|
|
||||||
# Android Profiling
|
# Android Profiling
|
||||||
*.hprof
|
*.hprof
|
||||||
|
|
||||||
|
#other
|
||||||
|
scripts/
|
||||||
|
|||||||
110
README.md
110
README.md
@@ -1,109 +1,41 @@
|
|||||||
# **Dantotsu** (🚧 ALPHA 🚧)
|
|
||||||
|
|
||||||
> ⚠️ **WARNING**: This project is in alpha stage. Things may not work as expected.
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
<img src="https://pbxt.replicate.delivery/2PX94viD6lJSDVayQrGyDH7CGu7IjQ6e8HEtOGDeelefXRdOC/out.png" alt="Dantotsu Banner" width=100% >
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
|
||||||
<a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
|
<a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
|
||||||
|
<a href="https://www.codefactor.io/repository/github/rebelonion/dantotsu"><img src="https://www.codefactor.io/repository/github/rebelonion/dantotsu/badge?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge" alt="CodeFactor" /></a>
|
||||||
|
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/discord/358599430502481920.svg?style=for-the-badge&logo=discord&colorB=7289DA"></a>
|
||||||
</p>
|
</p>
|
||||||
Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-of-the-art elegance. It is an <a href="https://anilist.co/">Anilist</a> only client, which also lets you stream-download Anime & Manga through extensions.
|
|
||||||
<br><br>
|
|
||||||
<i>Dantotsu (断トツ; Dan-totsu) literally means the best of the best in Japanese. Well, we would like to say this is the best open source app for anime and manga on Android, but hey, try it out yourself & judge!
|
|
||||||
</i>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
# **Dantotsu** 🌟
|
||||||
|
|
||||||
> **Warning**
|
Dantotsu is an [Anilist](https://anilist.co/) only client.
|
||||||
>
|
|
||||||
> Please do not attempt to upload Dantotsu or any of it's forks on Playstore or any other Android appstores on the internet. Doing so, may infringe their terms and conditions. This may result to legal action or immediate take-down of the app.
|
|
||||||
|
|
||||||
## Extension Status
|
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
|
||||||
|
|
||||||
| Type | Status |
|
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||||
| ---------------- | ------- |
|
|
||||||
| Anime Extensions | Working |
|
|
||||||
| Manga Extensions | "Working" |
|
|
||||||
| Light Novel Extensions | Not Working |
|
|
||||||
|
|
||||||
|
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||||
|
|
||||||
|
## WANT TO CONTRIBUTE? 🤝
|
||||||
|
|
||||||
## APP FEATURES
|
All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content - whatever you have to offer, we can use!
|
||||||
|
|
||||||
- Easy and functional way to both, watch anime and read manga, ad-free.
|
You can come hang out with our awesome community, request new features, and report any bugs or issues at our Discord server too. 📣
|
||||||
|
|
||||||
- A completely open source app with a nice UI & Animations :)
|
### OFFICIAL DISCORD SERVER 🚀
|
||||||
|
|
||||||
- Aniyomi extension support built right into the app.
|
|
||||||
|
|
||||||
- Synchronize anime and manga real-time with AniList and MyAnimeList. Easily categorise anime and manga based on your current status. (Powered by AniList)
|
|
||||||
|
|
||||||
- Find all shows using thoroughly and frequently updated list of all trending, popular and ongoing anime based on scores.
|
|
||||||
|
|
||||||
- View extensive details about anime shows, movies and manga titles. It also features ability to countdown to the next episode of airing anime. (Powered by AniList & MyAnimeList)
|
|
||||||
|
|
||||||
- Get notified when new episodes/chapters are released!
|
|
||||||
|
|
||||||
|
|
||||||
* **Available Anime sources:-**
|
|
||||||
NONE BUILT IN!
|
|
||||||
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
|
|
||||||
|
|
||||||
|
|
||||||
* **Available Manga sources:-**
|
|
||||||
NONE BUILT IN!
|
|
||||||
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
|
|
||||||
|
|
||||||
## Planned Stuff
|
|
||||||
|
|
||||||
- get app out of alpha
|
|
||||||
|
|
||||||
- Accent Color Change (RIP Hot Pink Supremacy.)
|
|
||||||
|
|
||||||
|
|
||||||
## Rejected Stuff (still rejected)
|
|
||||||
|
|
||||||
- Sources of any language except English
|
|
||||||
|
|
||||||
- News Section in the App
|
|
||||||
|
|
||||||
- Comment Section
|
|
||||||
|
|
||||||
|
|
||||||
## WANT TO CONTRIBUTE?
|
|
||||||
|
|
||||||
- All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content- whatever you have to offer, we can use it!
|
|
||||||
|
|
||||||
- You can come hang out with our awesome community and request new features and report any bugs or issue at our discord server too.
|
|
||||||
|
|
||||||
### Official Discord Server
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
<a href="https://discord.gg/4HPZ5nAWwM">
|
||||||
|
<img src="https://invidget.switchblade.xyz/4HPZ5nAWwM">
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## VISITORS
|
||||||
|
|
||||||
### VISIT FOR MORE INFORMATION:-
|
<img src="https://count.getloli.com/get/@:rebeloniondantotsu" alt=":rebeloniondantotsu" />
|
||||||
|
|
||||||
no website yet :(
|
## LICENSE 📜
|
||||||
|
|
||||||
## DISCLAIMER
|
|
||||||
|
|
||||||
* Dantotsu by itself only provides an anime and manga tracker and does not provide any anime or manga streaming or downloading capabilities.
|
|
||||||
|
|
||||||
* Dantotsu or any of its developer/staff don't host any of the content found inside Dantotsu. Any and all images and anime/manga information found in the app are taken from various public APIs (AniList, MyAnimeList, Kitsu).
|
|
||||||
|
|
||||||
* Furthermore, all of the anime/manga links found in Dantotsu are taken from various 3rd party plugins and have no affiliation with Dantotsu or its staff.
|
|
||||||
|
|
||||||
* Dantotsu or it's owners aren't liable for any misuse of any of the contents found inside or outside of the app and cannot be held accountable for the distribution of any of the contents found inside the app.
|
|
||||||
|
|
||||||
* By using Dantotsu, you comply to the fact that the developer of the app is not responsible for any of the contents found in the app. You also agree to the fact that you may not use Dantotsu to download or stream any copyrighted content.
|
|
||||||
|
|
||||||
* If the internet infringement issues are involved, please contact the source website. The developer does not assume any legal responsibility.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
||||||
|
|||||||
@@ -21,19 +21,20 @@ android {
|
|||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||||
versionName "0.1.2"
|
versionName "2.0.0-beta01-iv1"
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
//applicationIdSuffix ".beta"
|
applicationIdSuffix ".beta"
|
||||||
debuggable true
|
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
|
||||||
versionNameSuffix "." + gitCommitHash
|
debuggable System.getenv("CI") == null
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
|
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
|
||||||
debuggable false
|
debuggable false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
@@ -53,18 +54,20 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// Core
|
// Core
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.browser:browser:1.6.0'
|
implementation 'androidx.browser:browser:1.7.0'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation 'com.google.code.gson:gson:2.10'
|
||||||
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
|
||||||
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
|
implementation 'androidx.webkit:webkit:1.9.0'
|
||||||
|
|
||||||
// Glide
|
// Glide
|
||||||
ext.glide_version = '4.16.0'
|
ext.glide_version = '4.16.0'
|
||||||
@@ -76,26 +79,31 @@ dependencies {
|
|||||||
|
|
||||||
// FireBase
|
// FireBase
|
||||||
implementation platform('com.google.firebase:firebase-bom:32.2.3')
|
implementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||||
implementation 'com.google.firebase:firebase-analytics-ktx:21.3.0'
|
implementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
|
||||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.4.3'
|
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.0'
|
||||||
|
|
||||||
// Exoplayer
|
// Exoplayer
|
||||||
ext.exo_version = '1.1.1'
|
ext.exo_version = '1.2.0'
|
||||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||||
implementation "androidx.media3:media3-ui:$exo_version"
|
implementation "androidx.media3:media3-ui:$exo_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||||
implementation "androidx.media3:media3-session:$exo_version"
|
implementation "androidx.media3:media3-session:$exo_version"
|
||||||
|
//media3 casting
|
||||||
|
implementation "androidx.media3:media3-cast:$exo_version"
|
||||||
|
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.google.android.material:material:1.10.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||||
|
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||||
|
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||||
|
|
||||||
// string matching
|
// string matching
|
||||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||||
@@ -110,13 +118,14 @@ dependencies {
|
|||||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
|
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||||
implementation 'com.squareup.okio:okio:3.3.0'
|
implementation 'com.squareup.okio:okio:3.7.0'
|
||||||
implementation 'ch.acra:acra-http:5.9.7'
|
implementation 'ch.acra:acra-http:5.11.3'
|
||||||
implementation 'org.jsoup:jsoup:1.15.4'
|
implementation 'org.jsoup:jsoup:1.15.4'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.5.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
|
||||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
|
implementation 'app.cash.quickjs:quickjs-android:0.9.2'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
67
app/google-services.json
Normal file
67
app/google-services.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "1039200814590",
|
||||||
|
"project_id": "dantotsu-1e50f",
|
||||||
|
"storage_bucket": "dantotsu-1e50f.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:1039200814590:android:c372b8c1b92b825f1aacaf",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "ani.Dantotsu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "ani.dantotsu.beta"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "ani.dantotsu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Dantotsu α</string>
|
<string name="app_name">Dantotsu β</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
|
||||||
|
tools:ignore="LeanbackUsesWifi" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
@@ -17,23 +19,22 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
android:maxSdkVersion="32" />
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||||
<!-- For background jobs -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
|
||||||
|
|
||||||
<!-- For managing extensions -->
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
|
||||||
<!-- To view extension packages in API 30+ -->
|
<uses-permission
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<uses-permission
|
||||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
@@ -46,22 +47,37 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:banner="@mipmap/ic_banner_foreground"
|
||||||
|
android:icon="${icon_placeholder}"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="${icon_placeholder_round}"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Dantotsu"
|
android:theme="@style/Theme.Dantotsu"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="AllowBackup"
|
tools:ignore="AllowBackup">
|
||||||
android:banner="@drawable/ic_banner_foreground">
|
<receiver
|
||||||
|
android:name=".widgets.CurrentlyAiringWidget"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/currently_airing_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
<receiver android:name=".subcriptions.NotificationClickReceiver" />
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity"
|
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="application/epub+zip" />
|
<data android:mimeType="application/epub+zip" />
|
||||||
@@ -69,13 +85,11 @@
|
|||||||
<data android:mimeType="application/vnd.amazon.ebook" />
|
<data android:mimeType="application/vnd.amazon.ebook" />
|
||||||
<data android:mimeType="application/fb2+zip" />
|
<data android:mimeType="application/fb2+zip" />
|
||||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||||
|
|
||||||
<data android:pathPattern=".*\\.epub" />
|
<data android:pathPattern=".*\\.epub" />
|
||||||
<data android:pathPattern=".*\\.mobi" />
|
<data android:pathPattern=".*\\.mobi" />
|
||||||
<data android:pathPattern=".*\\.kf8" />
|
<data android:pathPattern=".*\\.kf8" />
|
||||||
<data android:pathPattern=".*\\.fb2" />
|
<data android:pathPattern=".*\\.fb2" />
|
||||||
<data android:pathPattern=".*\\.cbz" />
|
<data android:pathPattern=".*\\.cbz" />
|
||||||
|
|
||||||
<data android:scheme="content" />
|
<data android:scheme="content" />
|
||||||
<data android:scheme="file" />
|
<data android:scheme="file" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -101,9 +115,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".media.CalendarActivity"
|
android:name=".media.CalendarActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
<activity android:name="ani.dantotsu.media.user.ListActivity" />
|
<activity android:name=".media.user.ListActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity"
|
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/manga"
|
android:label="@string/manga"
|
||||||
@@ -116,7 +130,7 @@
|
|||||||
<activity android:name=".media.CharacterDetailsActivity" />
|
<activity android:name=".media.CharacterDetailsActivity" />
|
||||||
<activity android:name=".home.NoInternet" />
|
<activity android:name=".home.NoInternet" />
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.media.anime.ExoplayerView"
|
android:name=".media.anime.ExoplayerView"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -125,7 +139,7 @@
|
|||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
tools:targetApi="n" />
|
tools:targetApi="n" />
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.connections.anilist.Login"
|
android:name=".connections.anilist.Login"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -142,7 +156,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.connections.mal.Login"
|
android:name=".connections.mal.Login"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -158,8 +172,8 @@
|
|||||||
android:scheme="dantotsu" />
|
android:scheme="dantotsu" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
<activity android:name="ani.dantotsu.connections.discord.Login"
|
android:name=".connections.discord.Login"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -176,9 +190,26 @@
|
|||||||
<data android:host="discord.dantotsu.com" />
|
<data android:host="discord.dantotsu.com" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.connections.anilist.UrlMedia"
|
android:name=".others.webview.CookieCatcher"
|
||||||
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask">
|
||||||
|
<intent-filter android:label="Discord Login for Dantotsu">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="dantotsu" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="discord.dantotsu.com" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".connections.anilist.UrlMedia"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -214,22 +245,23 @@
|
|||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.Main" />
|
<action android:name="android.intent.action.Main" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".subcriptions.AlarmReceiver"
|
android:name=".subcriptions.AlarmReceiver"
|
||||||
@@ -255,18 +287,49 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service android:name=".download.video.MyDownloadService"
|
<service
|
||||||
android:exported="false">
|
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
||||||
|
android:permission="android.permission.BIND_REMOTEVIEWS"
|
||||||
|
android:exported="true" />
|
||||||
|
<service
|
||||||
|
android:name=".download.video.ExoplayerDownloadService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
<service
|
||||||
android:exported="false" />
|
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name=".download.manga.MangaDownloaderService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name=".download.novel.NovelDownloaderService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service android:name=".download.anime.AnimeDownloaderService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name=".connections.discord.DiscordService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
android:exported="true"/>
|
||||||
|
|
||||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
android:exported="false" />
|
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/ic_launcher_beta-playstore.png
Normal file
BIN
app/src/main/ic_launcher_beta-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -8,19 +8,36 @@ import androidx.multidex.MultiDex
|
|||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
|
||||||
import tachiyomi.core.util.system.logcat
|
|
||||||
import ani.dantotsu.others.DisabledReports
|
import ani.dantotsu.others.DisabledReports
|
||||||
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
|
import ani.dantotsu.parsers.MangaSources
|
||||||
|
import ani.dantotsu.parsers.NovelSources
|
||||||
|
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||||
|
import ani.dantotsu.settings.SettingsActivity
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import logcat.AndroidLogcatLogger
|
import logcat.AndroidLogcatLogger
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.LogcatLogger
|
import logcat.LogcatLogger
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
class App : MultiDexApplication() {
|
class App : MultiDexApplication() {
|
||||||
|
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||||
|
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||||
|
private lateinit var novelExtensionManager: NovelExtensionManager
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
MultiDex.install(this)
|
MultiDex.install(this)
|
||||||
@@ -38,21 +55,64 @@ class App : MultiDexApplication() {
|
|||||||
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
|
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
|
||||||
if (useMaterialYou) {
|
if (useMaterialYou) {
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
//TODO: HarmonizedColors
|
||||||
}
|
}
|
||||||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||||
|
|
||||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||||
initializeNetwork(baseContext)
|
getSharedPreferences(
|
||||||
|
getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).getBoolean("shared_user_id", true).let {
|
||||||
|
if (!it) return@let
|
||||||
|
val dUsername = getSharedPreferences(
|
||||||
|
getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).getString("discord_username", null)
|
||||||
|
val aUsername = getSharedPreferences(
|
||||||
|
getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).getString("anilist_username", null)
|
||||||
|
if (dUsername != null || aUsername != null) {
|
||||||
|
Firebase.crashlytics.setUserId("$dUsername - $aUsername")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FirebaseCrashlytics.getInstance().setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
||||||
|
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
Injekt.importModule(PreferenceModule(this))
|
Injekt.importModule(PreferenceModule(this))
|
||||||
|
|
||||||
|
initializeNetwork(baseContext)
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
if (!LogcatLogger.isInstalled) {
|
if (!LogcatLogger.isInstalled) {
|
||||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
animeExtensionManager = Injekt.get()
|
||||||
|
mangaExtensionManager = Injekt.get()
|
||||||
|
novelExtensionManager = Injekt.get()
|
||||||
|
|
||||||
|
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
animeScope.launch {
|
||||||
|
animeExtensionManager.findAvailableExtensions()
|
||||||
|
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||||
|
AnimeSources.init(animeExtensionManager.installedExtensionsFlow, this@App)
|
||||||
}
|
}
|
||||||
|
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
mangaScope.launch {
|
||||||
|
mangaExtensionManager.findAvailableExtensions()
|
||||||
|
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||||
|
MangaSources.init(mangaExtensionManager.installedExtensionsFlow, this@App)
|
||||||
|
}
|
||||||
|
val novelScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
novelScope.launch {
|
||||||
|
novelExtensionManager.findAvailableExtensions()
|
||||||
|
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||||
|
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun setupNotificationChannels() {
|
private fun setupNotificationChannels() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.animation.ObjectAnimator
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.DatePickerDialog
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -13,6 +15,7 @@ import android.content.res.Configuration
|
|||||||
import android.content.res.Resources.getSystem
|
import android.content.res.Resources.getSystem
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities.*
|
import android.net.NetworkCapabilities.*
|
||||||
@@ -23,11 +26,13 @@ import android.telephony.TelephonyManager
|
|||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.animation.*
|
import android.view.animation.*
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.math.MathUtils.clamp
|
import androidx.core.math.MathUtils.clamp
|
||||||
@@ -44,6 +49,8 @@ import ani.dantotsu.databinding.ItemCountDownBinding
|
|||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.parsers.ShowResponse
|
import ani.dantotsu.parsers.ShowResponse
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.subcriptions.NotificationClickReceiver
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
||||||
@@ -53,6 +60,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import com.google.android.material.internal.ViewUtils
|
import com.google.android.material.internal.ViewUtils
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
import java.io.*
|
import java.io.*
|
||||||
@@ -124,6 +133,13 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
|
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
|
||||||
|
//try to delete the file
|
||||||
|
try {
|
||||||
|
a?.deleteFile(fileName)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
FirebaseCrashlytics.getInstance().log("Failed to delete file $fileName")
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
}
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -132,7 +148,8 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
|
|||||||
fun initActivity(a: Activity) {
|
fun initActivity(a: Activity) {
|
||||||
val window = a.window
|
val window = a.window
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false) ?: UserInterfaceSettings().apply {
|
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false)
|
||||||
|
?: UserInterfaceSettings().apply {
|
||||||
saveData("ui_settings", this)
|
saveData("ui_settings", this)
|
||||||
}
|
}
|
||||||
uiSettings.darkMode.apply {
|
uiSettings.darkMode.apply {
|
||||||
@@ -146,7 +163,8 @@ fun initActivity(a: Activity) {
|
|||||||
}
|
}
|
||||||
if (uiSettings.immersiveMode) {
|
if (uiSettings.immersiveMode) {
|
||||||
if (navBarHeight == 0) {
|
if (navBarHeight == 0) {
|
||||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))?.apply {
|
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||||
|
?.apply {
|
||||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +178,8 @@ fun initActivity(a: Activity) {
|
|||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
if (statusBarHeight == 0) {
|
if (statusBarHeight == 0) {
|
||||||
val windowInsets = ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
val windowInsets =
|
||||||
|
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||||
if (windowInsets != null) {
|
if (windowInsets != null) {
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
statusBarHeight = insets.top
|
statusBarHeight = insets.top
|
||||||
@@ -188,10 +207,17 @@ fun Activity.hideStatusBar() {
|
|||||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
val window = dialog?.window
|
||||||
|
val decorView: View = window?.decorView ?: return
|
||||||
|
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
}
|
}
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
val theme = requireContext().theme
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurfaceInverse, typedValue, true)
|
||||||
|
window.navigationBarColor = typedValue.data
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun show(manager: FragmentManager, tag: String?) {
|
override fun show(manager: FragmentManager, tag: String?) {
|
||||||
@@ -202,9 +228,9 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isOnline(context: Context): Boolean {
|
fun isOnline(context: Context): Boolean {
|
||||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
val connectivityManager =
|
||||||
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
return tryWith {
|
return tryWith {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||||
return@tryWith if (cap != null) {
|
return@tryWith if (cap != null) {
|
||||||
when {
|
when {
|
||||||
@@ -220,7 +246,6 @@ fun isOnline(context: Context): Boolean {
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
} else false
|
} else false
|
||||||
} else true
|
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +263,8 @@ fun startMainActivity(activity: Activity, bundle: Bundle? = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) : DialogFragment(),
|
class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) :
|
||||||
|
DialogFragment(),
|
||||||
DatePickerDialog.OnDateSetListener {
|
DatePickerDialog.OnDateSetListener {
|
||||||
var dialog: DatePickerDialog
|
var dialog: DatePickerDialog
|
||||||
|
|
||||||
@@ -263,9 +289,20 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputFilterMinMax(private val min: Double, private val max: Double, private val status: AutoCompleteTextView? = null) :
|
class InputFilterMinMax(
|
||||||
|
private val min: Double,
|
||||||
|
private val max: Double,
|
||||||
|
private val status: AutoCompleteTextView? = null
|
||||||
|
) :
|
||||||
InputFilter {
|
InputFilter {
|
||||||
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
|
override fun filter(
|
||||||
|
source: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
dest: Spanned,
|
||||||
|
dstart: Int,
|
||||||
|
dend: Int
|
||||||
|
): CharSequence? {
|
||||||
try {
|
try {
|
||||||
val input = (dest.toString() + source.toString()).toDouble()
|
val input = (dest.toString() + source.toString()).toDouble()
|
||||||
if (isInRange(min, max, input)) return null
|
if (isInRange(min, max, input)) return null
|
||||||
@@ -288,11 +325,20 @@ class InputFilterMinMax(private val min: Double, private val max: Double, privat
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : ViewPager2.PageTransformer {
|
class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) :
|
||||||
|
ViewPager2.PageTransformer {
|
||||||
override fun transformPage(view: View, position: Float) {
|
override fun transformPage(view: View, position: Float) {
|
||||||
if (position == 0.0f && uiSettings.layoutAnimations) {
|
if (position == 0.0f && uiSettings.layoutAnimations) {
|
||||||
setAnimation(view.context, view, uiSettings, 300, floatArrayOf(1.3f, 1f, 1.3f, 1f), 0.5f to 0f)
|
setAnimation(
|
||||||
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f).setDuration((200 * uiSettings.animationSpeed).toLong()).start()
|
view.context,
|
||||||
|
view,
|
||||||
|
uiSettings,
|
||||||
|
300,
|
||||||
|
floatArrayOf(1.3f, 1f, 1.3f, 1f),
|
||||||
|
0.5f to 0f
|
||||||
|
)
|
||||||
|
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f)
|
||||||
|
.setDuration((200 * uiSettings.animationSpeed).toLong()).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,7 +373,11 @@ class FadingEdgeRecyclerView : RecyclerView {
|
|||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
)
|
||||||
|
|
||||||
override fun isPaddingOffsetRequired(): Boolean {
|
override fun isPaddingOffsetRequired(): Boolean {
|
||||||
return !clipToPadding
|
return !clipToPadding
|
||||||
@@ -419,15 +469,30 @@ fun String.findBetween(a: String, b: String): String? {
|
|||||||
|
|
||||||
fun ImageView.loadImage(url: String?, size: Int = 0) {
|
fun ImageView.loadImage(url: String?, size: Int = 0) {
|
||||||
if (!url.isNullOrEmpty()) {
|
if (!url.isNullOrEmpty()) {
|
||||||
|
val localFile = File(url)
|
||||||
|
if (localFile.exists()) {
|
||||||
|
loadLocalImage(localFile, size)
|
||||||
|
} else {
|
||||||
loadImage(FileUrl(url), size)
|
loadImage(FileUrl(url), size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||||
if (file?.url?.isNotEmpty() == true) {
|
if (file?.url?.isNotEmpty() == true) {
|
||||||
tryWith {
|
tryWith {
|
||||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size).into(this)
|
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||||
|
.into(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
||||||
|
if (file?.exists() == true) {
|
||||||
|
tryWith {
|
||||||
|
Glide.with(this.context).load(file).transition(withCrossFade()).override(size)
|
||||||
|
.into(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,7 +550,12 @@ abstract class GesturesListener : GestureDetector.SimpleOnGestureListener() {
|
|||||||
return super.onDoubleTap(e)
|
return super.onDoubleTap(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
override fun onScroll(
|
||||||
|
e1: MotionEvent?,
|
||||||
|
e2: MotionEvent,
|
||||||
|
distanceX: Float,
|
||||||
|
distanceY: Float
|
||||||
|
): Boolean {
|
||||||
onScrollYClick(distanceY)
|
onScrollYClick(distanceY)
|
||||||
onScrollXClick(distanceX)
|
onScrollXClick(distanceX)
|
||||||
return super.onScroll(e1, e2, distanceX, distanceY)
|
return super.onScroll(e1, e2, distanceX, distanceY)
|
||||||
@@ -551,7 +621,7 @@ fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
|
|||||||
"$APPLICATION_ID.provider",
|
"$APPLICATION_ID.provider",
|
||||||
saveImage(
|
saveImage(
|
||||||
bitmap,
|
bitmap,
|
||||||
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS,
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||||
title
|
title
|
||||||
) ?: return
|
) ?: return
|
||||||
)
|
)
|
||||||
@@ -574,13 +644,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
|||||||
|
|
||||||
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
||||||
val imageFile = File(path, "$imageFileName.png")
|
val imageFile = File(path, "$imageFileName.png")
|
||||||
return tryWith {
|
return try {
|
||||||
val fOut: OutputStream = FileOutputStream(imageFile)
|
val fOut: OutputStream = FileOutputStream(imageFile)
|
||||||
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
|
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
|
||||||
fOut.close()
|
fOut.close()
|
||||||
scanFile(imageFile.absolutePath, currContext()!!)
|
scanFile(imageFile.absolutePath, currContext()!!)
|
||||||
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
|
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
|
||||||
imageFile
|
imageFile
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snackString("Failed to save image: ${e.localizedMessage}")
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,13 +696,19 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
fun countDown(media: Media, view: ViewGroup) {
|
fun countDown(media: Media, view: ViewGroup) {
|
||||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) {
|
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
|
||||||
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||||
view.addView(v.root, 0)
|
view.addView(v.root, 0)
|
||||||
v.mediaCountdownText.text =
|
v.mediaCountdownText.text =
|
||||||
currActivity()?.getString(R.string.episode_release_countdown, media.anime.nextAiringEpisode!! + 1)
|
currActivity()?.getString(
|
||||||
|
R.string.episode_release_countdown,
|
||||||
|
media.anime.nextAiringEpisode!! + 1
|
||||||
|
)
|
||||||
|
|
||||||
object : CountDownTimer((media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(), 1000) {
|
object : CountDownTimer(
|
||||||
|
(media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(),
|
||||||
|
1000
|
||||||
|
) {
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
val a = millisUntilFinished / 1000
|
val a = millisUntilFinished / 1000
|
||||||
v.mediaCountdown.text = currActivity()?.getString(
|
v.mediaCountdown.text = currActivity()?.getString(
|
||||||
@@ -720,16 +799,22 @@ fun toast(string: String?) {
|
|||||||
if (string != null) {
|
if (string != null) {
|
||||||
logger(string)
|
logger(string)
|
||||||
MainScope().launch {
|
MainScope().launch {
|
||||||
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT).show()
|
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
|
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
|
||||||
|
try { //I have no idea why this sometimes crashes for some people...
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
(activity ?: currActivity())?.apply {
|
(activity ?: currActivity())?.apply {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG)
|
val snackBar = Snackbar.make(
|
||||||
|
window.decorView.findViewById(android.R.id.content),
|
||||||
|
s,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
)
|
||||||
snackBar.view.apply {
|
snackBar.view.apply {
|
||||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||||
@@ -752,9 +837,14 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
|
|||||||
}
|
}
|
||||||
logger(s)
|
logger(s)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger(e.stackTraceToString())
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) : ArrayAdapter<T>(context, layoutId, items) {
|
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
|
||||||
|
ArrayAdapter<T>(context, layoutId, items) {
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
val view = super.getView(position, convertView, parent)
|
val view = super.getView(position, convertView, parent)
|
||||||
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
|
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
|
||||||
@@ -775,12 +865,17 @@ class SpinnerNoSwipe : androidx.appcompat.widget.AppCompatSpinner {
|
|||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
) {
|
||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setup() {
|
private fun setup() {
|
||||||
mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
mGestureDetector =
|
||||||
|
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
return performClick()
|
return performClick()
|
||||||
}
|
}
|
||||||
@@ -828,7 +923,11 @@ fun getCurrentBrightnessValue(context: Context): Float {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCur(): Float {
|
fun getCur(): Float {
|
||||||
return Settings.System.getInt(context.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 127).toFloat()
|
return Settings.System.getInt(
|
||||||
|
context.contentResolver,
|
||||||
|
Settings.System.SCREEN_BRIGHTNESS,
|
||||||
|
127
|
||||||
|
).toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
return brightnessConverter(getCur() / getMax(), true)
|
return brightnessConverter(getCur() / getMax(), true)
|
||||||
@@ -859,6 +958,33 @@ fun checkCountry(context: Context): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val INCOGNITO_CHANNEL_ID = 26
|
||||||
|
|
||||||
|
@SuppressLint("LaunchActivityFromNotification")
|
||||||
|
fun incognitoNotification(context: Context) {
|
||||||
|
val notificationManager =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val incognito = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("incognito", false)
|
||||||
|
if (incognito) {
|
||||||
|
val intent = Intent(context, NotificationClickReceiver::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context, 0, intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val builder = NotificationCompat.Builder(context, Notifications.CHANNEL_INCOGNITO_MODE)
|
||||||
|
.setSmallIcon(R.drawable.ic_incognito_24)
|
||||||
|
.setContentTitle("Incognito Mode")
|
||||||
|
.setContentText("Disable Incognito Mode")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setOngoing(true)
|
||||||
|
notificationManager.notify(INCOGNITO_CHANNEL_ID, builder.build())
|
||||||
|
} else {
|
||||||
|
notificationManager.cancel(INCOGNITO_CHANNEL_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun View.pop() {
|
suspend fun View.pop() {
|
||||||
currActivity()?.runOnUiThread {
|
currActivity()?.runOnUiThread {
|
||||||
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
|
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
package ani.dantotsu
|
package ani.dantotsu
|
||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnticipateInterpolator
|
import android.view.animation.AnticipateInterpolator
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.doOnAttach
|
import androidx.core.view.doOnAttach
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
@@ -26,12 +30,14 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.offline.Download
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||||
import ani.dantotsu.databinding.ActivityMainBinding
|
import ani.dantotsu.databinding.ActivityMainBinding
|
||||||
import ani.dantotsu.databinding.SplashScreenBinding
|
import ani.dantotsu.databinding.SplashScreenBinding
|
||||||
|
import ani.dantotsu.download.video.Helper
|
||||||
import ani.dantotsu.home.AnimeFragment
|
import ani.dantotsu.home.AnimeFragment
|
||||||
import ani.dantotsu.home.HomeFragment
|
import ani.dantotsu.home.HomeFragment
|
||||||
import ani.dantotsu.home.LoginFragment
|
import ani.dantotsu.home.LoginFragment
|
||||||
@@ -39,52 +45,109 @@ import ani.dantotsu.home.MangaFragment
|
|||||||
import ani.dantotsu.home.NoInternet
|
import ani.dantotsu.home.NoInternet
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.others.CustomBottomDialog
|
import ani.dantotsu.others.CustomBottomDialog
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.others.SharedPreferenceBooleanLiveData
|
||||||
|
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
|
||||||
private val scope = lifecycleScope
|
private val scope = lifecycleScope
|
||||||
private var load = false
|
private var load = false
|
||||||
|
|
||||||
private var uiSettings = UserInterfaceSettings()
|
private var uiSettings = UserInterfaceSettings()
|
||||||
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
|
|
||||||
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
|
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
|
LangSet.setLocale(this)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
//get FRAGMENT_CLASS_NAME from intent
|
||||||
|
val FRAGMENT_CLASS_NAME = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||||
animeScope.launch {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
animeExtensionManager.findAvailableExtensions()
|
|
||||||
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||||
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||||
|
backgroundDrawable.setColor(semiTransparentColor)
|
||||||
|
_bottomBar.background = backgroundDrawable
|
||||||
}
|
}
|
||||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
mangaScope.launch {
|
val colorOverflow = sharedPreferences.getBoolean("colorOverflow", false)
|
||||||
mangaExtensionManager.findAvailableExtensions()
|
if (!colorOverflow) {
|
||||||
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val offset = try {
|
||||||
|
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||||
|
resources.getDimensionPixelSize(statusBarHeightId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
statusBarHeight
|
||||||
|
}
|
||||||
|
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
layoutParams.topMargin = 11 * offset / 12
|
||||||
|
binding.incognito.layoutParams = layoutParams
|
||||||
|
incognitoLiveData = SharedPreferenceBooleanLiveData(
|
||||||
|
sharedPreferences,
|
||||||
|
"incognito",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
incognitoLiveData.observe(this) {
|
||||||
|
if (it) {
|
||||||
|
val slideDownAnim = ObjectAnimator.ofFloat(
|
||||||
|
binding.incognito,
|
||||||
|
View.TRANSLATION_Y,
|
||||||
|
-(binding.incognito.height.toFloat() + statusBarHeight),
|
||||||
|
0f
|
||||||
|
)
|
||||||
|
slideDownAnim.duration = 200
|
||||||
|
slideDownAnim.start()
|
||||||
|
binding.incognito.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
val slideUpAnim = ObjectAnimator.ofFloat(
|
||||||
|
binding.incognito,
|
||||||
|
View.TRANSLATION_Y,
|
||||||
|
0f,
|
||||||
|
-(binding.incognito.height.toFloat() + statusBarHeight)
|
||||||
|
)
|
||||||
|
slideUpAnim.duration = 200
|
||||||
|
slideUpAnim.start()
|
||||||
|
//wait for animation to finish
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed(
|
||||||
|
{ binding.incognito.visibility = View.GONE },
|
||||||
|
200
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
incognitoNotification(this)
|
||||||
|
|
||||||
var doubleBackToExitPressedOnce = false
|
var doubleBackToExitPressedOnce = false
|
||||||
onBackPressedDispatcher.addCallback(this) {
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
if (doubleBackToExitPressedOnce) {
|
if (doubleBackToExitPressedOnce) {
|
||||||
@@ -98,17 +161,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val preferences: SourcePreferences = Injekt.get()
|
||||||
|
if (preferences.animeExtensionUpdatesCount().get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0) {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
"You have extension updates available!",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
binding.root.isMotionEventSplittingEnabled = false
|
binding.root.isMotionEventSplittingEnabled = false
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||||
binding.root.addView(splash.root)
|
binding.root.addView(splash.root)
|
||||||
(splash.splashImage.drawable as Animatable).start()
|
(splash.splashImage.drawable as Animatable).start()
|
||||||
|
|
||||||
// Wait for 2 seconds (2000 milliseconds)
|
delay(1200)
|
||||||
delay(2000)
|
|
||||||
|
|
||||||
// Now perform the animation
|
|
||||||
ObjectAnimator.ofFloat(
|
ObjectAnimator.ofFloat(
|
||||||
splash.root,
|
splash.root,
|
||||||
View.TRANSLATION_Y,
|
View.TRANSLATION_Y,
|
||||||
@@ -121,32 +192,65 @@ class MainActivity : AppCompatActivity() {
|
|||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
splashScreen.setOnExitAnimationListener { splashScreenView ->
|
||||||
|
ObjectAnimator.ofFloat(
|
||||||
|
splashScreenView,
|
||||||
|
View.TRANSLATION_Y,
|
||||||
|
0f,
|
||||||
|
-splashScreenView.height.toFloat()
|
||||||
|
).apply {
|
||||||
|
interpolator = AnticipateInterpolator()
|
||||||
|
duration = 200L
|
||||||
|
doOnEnd { splashScreenView.remove() }
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
binding.root.doOnAttach {
|
binding.root.doOnAttach {
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||||
selectedOption = uiSettings.defaultStartUpTab
|
selectedOption = if (FRAGMENT_CLASS_NAME != null) {
|
||||||
binding.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
when (FRAGMENT_CLASS_NAME) {
|
||||||
|
AnimeFragment::class.java.name -> 0
|
||||||
|
HomeFragment::class.java.name -> 1
|
||||||
|
MangaFragment::class.java.name -> 2
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uiSettings.defaultStartUpTab
|
||||||
|
}
|
||||||
|
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = navBarHeight
|
bottomMargin = navBarHeight
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val offlineMode = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("offlineMode", false)
|
||||||
if (!isOnline(this)) {
|
if (!isOnline(this)) {
|
||||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||||
startActivity(Intent(this, NoInternet::class.java))
|
startActivity(Intent(this, NoInternet::class.java))
|
||||||
|
} else {
|
||||||
|
if (offlineMode) {
|
||||||
|
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||||
|
startActivity(Intent(this, NoInternet::class.java))
|
||||||
} else {
|
} else {
|
||||||
val model: AnilistHomeViewModel by viewModels()
|
val model: AnilistHomeViewModel by viewModels()
|
||||||
model.genres.observe(this) {
|
model.genres.observe(this) { it ->
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
if (it) {
|
if (it) {
|
||||||
val navbar = binding.navbar
|
val navbar = binding.includedNavbar.navbar
|
||||||
bottomBar = navbar
|
bottomBar = navbar
|
||||||
navbar.visibility = View.VISIBLE
|
navbar.visibility = View.VISIBLE
|
||||||
binding.mainProgressBar.visibility = View.GONE
|
binding.mainProgressBar.visibility = View.GONE
|
||||||
val mainViewPager = binding.viewpager
|
val mainViewPager = binding.viewpager
|
||||||
mainViewPager.isUserInputEnabled = false
|
mainViewPager.isUserInputEnabled = false
|
||||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
mainViewPager.adapter =
|
||||||
|
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||||
navbar.setOnTabSelectListener(object :
|
navbar.setOnTabSelectListener(object :
|
||||||
AnimatedBottomBar.OnTabSelectListener {
|
AnimatedBottomBar.OnTabSelectListener {
|
||||||
@@ -162,7 +266,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
navbar.selectTabAt(selectedOption)
|
navbar.selectTabAt(selectedOption)
|
||||||
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
|
mainViewPager.post {
|
||||||
|
mainViewPager.setCurrentItem(
|
||||||
|
selectedOption,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.mainProgressBar.visibility = View.GONE
|
binding.mainProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -225,8 +334,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
//TODO: Remove this
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||||
|
val downloadCursor = index.getDownloads()
|
||||||
|
while (downloadCursor.moveToNext()) {
|
||||||
|
val download = downloadCursor.download
|
||||||
|
Log.e("Downloader", download.request.uri.toString())
|
||||||
|
Log.e("Downloader", download.request.id.toString())
|
||||||
|
Log.e("Downloader", download.request.mimeType.toString())
|
||||||
|
Log.e("Downloader", download.request.data.size.toString())
|
||||||
|
Log.e("Downloader", download.bytesDownloaded.toString())
|
||||||
|
Log.e("Downloader", download.state.toString())
|
||||||
|
Log.e("Downloader", download.failureReason.toString())
|
||||||
|
|
||||||
|
if (download.state == Download.STATE_FAILED) { //simple cleanup
|
||||||
|
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//ViewPager
|
//ViewPager
|
||||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||||
|
|||||||
@@ -8,58 +8,51 @@ import ani.dantotsu.others.webview.WebViewBottomDialog
|
|||||||
import com.lagradost.nicehttp.Requests
|
import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.ResponseParser
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
import com.lagradost.nicehttp.addGenericDns
|
import com.lagradost.nicehttp.addGenericDns
|
||||||
import kotlinx.coroutines.*
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.InternalSerializationApi
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import okhttp3.Cache
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.io.File
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KFunction
|
import kotlin.reflect.KFunction
|
||||||
|
|
||||||
val defaultHeaders = mapOf(
|
lateinit var defaultHeaders: Map<String, String>
|
||||||
"User-Agent" to
|
|
||||||
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
|
|
||||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
|
||||||
)
|
|
||||||
lateinit var cache: Cache
|
|
||||||
|
|
||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var okHttpClient: OkHttpClient
|
||||||
lateinit var client: Requests
|
lateinit var client: Requests
|
||||||
|
|
||||||
fun initializeNetwork(context: Context) {
|
fun initializeNetwork(context: Context) {
|
||||||
val dns = loadData<Int>("settings_dns")
|
|
||||||
cache = Cache(
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
File(context.cacheDir, "http_cache"),
|
|
||||||
5 * 1024L * 1024L // 5 MiB
|
defaultHeaders = mapOf(
|
||||||
|
"User-Agent" to
|
||||||
|
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
||||||
|
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||||
)
|
)
|
||||||
okHttpClient = OkHttpClient.Builder()
|
|
||||||
.followRedirects(true)
|
okHttpClient = networkHelper.client
|
||||||
.followSslRedirects(true)
|
|
||||||
.cache(cache)
|
|
||||||
.apply {
|
|
||||||
when (dns) {
|
|
||||||
1 -> addGoogleDns()
|
|
||||||
2 -> addCloudFlareDns()
|
|
||||||
3 -> addAdGuardDns()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
client = Requests(
|
client = Requests(
|
||||||
okHttpClient,
|
networkHelper.client,
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
defaultCacheTime = 6,
|
defaultCacheTime = 6,
|
||||||
defaultCacheTimeUnit = TimeUnit.HOURS,
|
defaultCacheTimeUnit = TimeUnit.HOURS,
|
||||||
responseParser = Mapper
|
responseParser = Mapper
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Mapper : ResponseParser {
|
object Mapper : ResponseParser {
|
||||||
@@ -122,7 +115,11 @@ fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> tryWithSuspend(post: Boolean = false, snackbar: Boolean = true, call: suspend () -> T): T? {
|
suspend fun <T> tryWithSuspend(
|
||||||
|
post: Boolean = false,
|
||||||
|
snackbar: Boolean = true,
|
||||||
|
call: suspend () -> T
|
||||||
|
): T? {
|
||||||
return try {
|
return try {
|
||||||
call.invoke()
|
call.invoke()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -209,7 +206,8 @@ suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, St
|
|||||||
map = it
|
map = it
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
val fragmentManager = (currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
|
val fragmentManager =
|
||||||
|
(currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
|
||||||
webViewDialog.show(fragmentManager, "web-view")
|
webViewDialog.show(fragmentManager, "web-view")
|
||||||
delay(0)
|
delay(0)
|
||||||
latch.await(2, TimeUnit.MINUTES)
|
latch.await(2, TimeUnit.MINUTES)
|
||||||
|
|||||||
@@ -3,16 +3,26 @@ package ani.dantotsu.aniyomi.anime.custom
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
|
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||||
|
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
@@ -20,14 +30,20 @@ import uy.kohesive.injekt.api.addSingletonFactory
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
|
addSingletonFactory { DownloadsManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { NetworkHelper(app, get()) }
|
addSingletonFactory { NetworkHelper(app, get()) }
|
||||||
|
|
||||||
addSingletonFactory { AnimeExtensionManager(app) }
|
addSingletonFactory { AnimeExtensionManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { MangaExtensionManager(app) }
|
addSingletonFactory { MangaExtensionManager(app) }
|
||||||
|
addSingletonFactory { NovelExtensionManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||||
|
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||||
|
|
||||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
addSingleton(sharedPreferences)
|
addSingleton(sharedPreferences)
|
||||||
@@ -39,7 +55,14 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSingletonFactory { StandaloneDatabaseProvider(app) }
|
||||||
|
|
||||||
addSingletonFactory { MangaCache() }
|
addSingletonFactory { MangaCache() }
|
||||||
|
|
||||||
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
|
get<AnimeSourceManager>()
|
||||||
|
get<MangaSourceManager>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import ani.dantotsu.toast
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
fun updateProgress(media: Media, number: String) {
|
fun updateProgress(media: Media, number: String) {
|
||||||
|
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
|
||||||
|
?.getBoolean("incognito", false) ?: false
|
||||||
|
if (!incognito) {
|
||||||
if (Anilist.userid != null) {
|
if (Anilist.userid != null) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val a = number.toFloatOrNull()?.roundToInt()
|
val a = number.toFloatOrNull()?.toInt()
|
||||||
if (a != media.userProgress) {
|
if ((a ?: 0) > (media.userProgress ?: 0)) {
|
||||||
Anilist.mutation.editList(
|
Anilist.mutation.editList(
|
||||||
media.id,
|
media.id,
|
||||||
a,
|
a,
|
||||||
@@ -36,4 +38,7 @@ fun updateProgress(media: Media, number: String) {
|
|||||||
} else {
|
} else {
|
||||||
toast(currContext()?.getString(R.string.login_anilist_account))
|
toast(currContext()?.getString(R.string.login_anilist_account))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
toast("Sneaky sneaky :3")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import ani.dantotsu.currContext
|
|||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
|
||||||
object Anilist {
|
object Anilist {
|
||||||
val query: AnilistQueries = AnilistQueries()
|
val query: AnilistQueries = AnilistQueries()
|
||||||
@@ -29,7 +29,12 @@ object Anilist {
|
|||||||
var tags: Map<Boolean, List<String>>? = null
|
var tags: Map<Boolean, List<String>>? = null
|
||||||
|
|
||||||
val sortBy = listOf(
|
val sortBy = listOf(
|
||||||
"SCORE_DESC","POPULARITY_DESC","TRENDING_DESC","TITLE_ENGLISH","TITLE_ENGLISH_DESC","SCORE"
|
"SCORE_DESC",
|
||||||
|
"POPULARITY_DESC",
|
||||||
|
"TRENDING_DESC",
|
||||||
|
"TITLE_ENGLISH",
|
||||||
|
"TITLE_ENGLISH_DESC",
|
||||||
|
"SCORE"
|
||||||
)
|
)
|
||||||
|
|
||||||
val seasons = listOf(
|
val seasons = listOf(
|
||||||
@@ -132,7 +137,12 @@ object Anilist {
|
|||||||
if (token != null || force) {
|
if (token != null || force) {
|
||||||
if (token != null && useToken) headers["Authorization"] = "Bearer $token"
|
if (token != null && useToken) headers["Authorization"] = "Bearer $token"
|
||||||
|
|
||||||
val json = client.post("https://graphql.anilist.co/", headers, data = data, cacheTime = cache ?: 10)
|
val json = client.post(
|
||||||
|
"https://graphql.anilist.co/",
|
||||||
|
headers,
|
||||||
|
data = data,
|
||||||
|
cacheTime = cache ?: 10
|
||||||
|
)
|
||||||
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||||
if (show) println("Response : ${json.text}")
|
if (show) println("Response : ${json.text}")
|
||||||
json.parsed()
|
json.parsed()
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
package ani.dantotsu.connections.anilist
|
package ani.dantotsu.connections.anilist
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.checkGenreTime
|
||||||
|
import ani.dantotsu.checkId
|
||||||
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
||||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.connections.anilist.api.Page
|
import ani.dantotsu.connections.anilist.api.Page
|
||||||
import ani.dantotsu.connections.anilist.api.Query
|
import ani.dantotsu.connections.anilist.api.Query
|
||||||
import ani.dantotsu.checkGenreTime
|
|
||||||
import ani.dantotsu.checkId
|
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.isOnline
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.media.Author
|
import ani.dantotsu.media.Author
|
||||||
@@ -33,6 +35,13 @@ class AnilistQueries {
|
|||||||
}.also { println("time : $it") }
|
}.also { println("time : $it") }
|
||||||
val user = response?.data?.user ?: return false
|
val user = response?.data?.user ?: return false
|
||||||
|
|
||||||
|
currContext()?.let {
|
||||||
|
it.getSharedPreferences(it.getString(R.string.preference_file_key), Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putString("anilist_username", user.name)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
Anilist.userid = user.id
|
Anilist.userid = user.id
|
||||||
Anilist.username = user.name
|
Anilist.username = user.name
|
||||||
Anilist.bg = user.bannerImage
|
Anilist.bg = user.bannerImage
|
||||||
@@ -114,8 +123,12 @@ class AnilistQueries {
|
|||||||
image = i.node?.image?.medium,
|
image = i.node?.image?.medium,
|
||||||
banner = media.banner ?: media.cover,
|
banner = media.banner ?: media.cover,
|
||||||
role = when (i.role.toString()) {
|
role = when (i.role.toString()) {
|
||||||
"MAIN" -> currContext()?.getString(R.string.main_role) ?: "MAIN"
|
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role) ?: "SUPPORTING"
|
?: "MAIN"
|
||||||
|
|
||||||
|
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
|
||||||
|
?: "SUPPORTING"
|
||||||
|
|
||||||
else -> i.role.toString()
|
else -> i.role.toString()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -129,11 +142,16 @@ class AnilistQueries {
|
|||||||
val m = Media(mediaEdge)
|
val m = Media(mediaEdge)
|
||||||
media.relations?.add(m)
|
media.relations?.add(m)
|
||||||
if (m.relation == "SEQUEL") {
|
if (m.relation == "SEQUEL") {
|
||||||
media.sequel = if ((media.sequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.sequel
|
media.sequel =
|
||||||
|
if ((media.sequel?.popularity ?: 0) < (m.popularity
|
||||||
|
?: 0)
|
||||||
|
) m else media.sequel
|
||||||
|
|
||||||
} else if (m.relation == "PREQUEL") {
|
} else if (m.relation == "PREQUEL") {
|
||||||
media.prequel =
|
media.prequel =
|
||||||
if ((media.prequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.prequel
|
if ((media.prequel?.popularity ?: 0) < (m.popularity
|
||||||
|
?: 0)
|
||||||
|
) m else media.prequel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
media.relations?.sortByDescending { it.popularity }
|
media.relations?.sortByDescending { it.popularity }
|
||||||
@@ -199,17 +217,19 @@ class AnilistQueries {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
media.anime.nextAiringEpisodeTime = fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
media.anime.nextAiringEpisodeTime =
|
||||||
|
fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
||||||
|
|
||||||
fetchedMedia.externalLinks?.forEach { i ->
|
fetchedMedia.externalLinks?.forEach { i ->
|
||||||
when (i.site.lowercase()) {
|
when (i.site.lowercase()) {
|
||||||
"youtube" -> media.anime.youtube = i.url
|
"youtube" -> media.anime.youtube = i.url
|
||||||
"crunchyroll" -> media.crunchySlug = i.url?.split("/")?.getOrNull(3)
|
"crunchyroll" -> media.crunchySlug =
|
||||||
|
i.url?.split("/")?.getOrNull(3)
|
||||||
|
|
||||||
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (media.manga != null) {
|
||||||
else if (media.manga != null) {
|
|
||||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||||
media.manga.author = Author(
|
media.manga.author = Author(
|
||||||
it.id.toString(),
|
it.id.toString(),
|
||||||
@@ -228,9 +248,11 @@ class AnilistQueries {
|
|||||||
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (currContext()?.let { isOnline(it) } == true) {
|
||||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val mal = async {
|
val mal = async {
|
||||||
if (media.idMAL != null) {
|
if (media.idMAL != null) {
|
||||||
MalScraper.loadMedia(media)
|
MalScraper.loadMedia(media)
|
||||||
@@ -361,7 +383,11 @@ class AnilistQueries {
|
|||||||
return default
|
return default
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMediaLists(anime: Boolean, userId: Int, sortOrder: String? = null): MutableMap<String, ArrayList<Media>> {
|
suspend fun getMediaLists(
|
||||||
|
anime: Boolean,
|
||||||
|
userId: Int,
|
||||||
|
sortOrder: String? = null
|
||||||
|
): MutableMap<String, ArrayList<Media>> {
|
||||||
val response =
|
val response =
|
||||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
|
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
|
||||||
val sorted = mutableMapOf<String, ArrayList<Media>>()
|
val sorted = mutableMapOf<String, ArrayList<Media>>()
|
||||||
@@ -395,11 +421,19 @@ class AnilistQueries {
|
|||||||
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
||||||
|
|
||||||
sorted["All"] = all
|
sorted["All"] = all
|
||||||
|
val listsort = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
val sort = sortOrder ?: options?.rowOrder
|
?.getString("sort_order", "score")
|
||||||
|
val sort = listsort ?: sortOrder ?: options?.rowOrder
|
||||||
for (i in sorted.keys) {
|
for (i in sorted.keys) {
|
||||||
when (sort) {
|
when (sort) {
|
||||||
"score" -> sorted[i]?.sortWith { b, a -> compareValuesBy(a, b, { it.userScore }, { it.meanScore }) }
|
"score" -> sorted[i]?.sortWith { b, a ->
|
||||||
|
compareValuesBy(
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
{ it.userScore },
|
||||||
|
{ it.meanScore })
|
||||||
|
}
|
||||||
|
|
||||||
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
||||||
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
|
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
|
||||||
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
||||||
@@ -564,13 +598,31 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
||||||
${
|
${
|
||||||
if (excludedGenres?.isNotEmpty() == true)
|
if (excludedGenres?.isNotEmpty() == true)
|
||||||
""","excludedGenres":[${excludedGenres.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
""","excludedGenres":[${
|
||||||
|
excludedGenres.joinToString {
|
||||||
|
"\"${
|
||||||
|
it.replace(
|
||||||
|
"Not ",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}\""
|
||||||
|
}
|
||||||
|
}]"""
|
||||||
else ""
|
else ""
|
||||||
}
|
}
|
||||||
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
|
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
|
||||||
${
|
${
|
||||||
if (excludedTags?.isNotEmpty() == true)
|
if (excludedTags?.isNotEmpty() == true)
|
||||||
""","excludedTags":[${excludedTags.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
""","excludedTags":[${
|
||||||
|
excludedTags.joinToString {
|
||||||
|
"\"${
|
||||||
|
it.replace(
|
||||||
|
"Not ",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}\""
|
||||||
|
}
|
||||||
|
}]"""
|
||||||
else ""
|
else ""
|
||||||
}
|
}
|
||||||
}""".replace("\n", " ").replace(""" """, "")
|
}""".replace("\n", " ").replace(""" """, "")
|
||||||
@@ -822,7 +874,8 @@ Page(page:$page,perPage:50) {
|
|||||||
var page = 0
|
var page = 0
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
page++
|
page++
|
||||||
hasNextPage = executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
hasNextPage =
|
||||||
|
executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
||||||
it.edges?.forEach { i ->
|
it.edges?.forEach { i ->
|
||||||
i.node?.apply {
|
i.node?.apply {
|
||||||
val status = status.toString()
|
val status = status.toString()
|
||||||
@@ -896,7 +949,10 @@ Page(page:$page,perPage:50) {
|
|||||||
|
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
page++
|
page++
|
||||||
hasNextPage = executeQuery<Query.Author>(query(page), force = true)?.data?.author?.staffMedia?.let {
|
hasNextPage = executeQuery<Query.Author>(
|
||||||
|
query(page),
|
||||||
|
force = true
|
||||||
|
)?.data?.author?.staffMedia?.let {
|
||||||
it.edges?.forEach { i ->
|
it.edges?.forEach { i ->
|
||||||
i.node?.apply {
|
i.node?.apply {
|
||||||
val status = status.toString()
|
val status = status.toString()
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.discord.Discord
|
import ani.dantotsu.connections.discord.Discord
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.others.AppUpdater
|
import ani.dantotsu.others.AppUpdater
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
@@ -19,9 +19,16 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
if (Discord.userid == null && Discord.token != null) {
|
val sharedPref = context.getSharedPreferences(
|
||||||
if (!Discord.getUserData())
|
context.getString(R.string.preference_file_key),
|
||||||
snackString(context.getString(R.string.error_loading_discord_user_data))
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
val token = sharedPref.getString("discord_token", null)
|
||||||
|
val userid = sharedPref.getString("discord_id", null)
|
||||||
|
if (userid == null && token != null) {
|
||||||
|
/*if (!Discord.getUserData())
|
||||||
|
snackString(context.getString(R.string.error_loading_discord_user_data))*/
|
||||||
|
//TODO: Discord.getUserData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,35 +49,53 @@ suspend fun getUserId(context: Context, block: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AnilistHomeViewModel : ViewModel() {
|
class AnilistHomeViewModel : ViewModel() {
|
||||||
private val listImages: MutableLiveData<ArrayList<String?>> = MutableLiveData<ArrayList<String?>>(arrayListOf())
|
private val listImages: MutableLiveData<ArrayList<String?>> =
|
||||||
|
MutableLiveData<ArrayList<String?>>(arrayListOf())
|
||||||
|
|
||||||
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
||||||
suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages())
|
suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages())
|
||||||
|
|
||||||
private val animeContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val animeContinue: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
||||||
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
||||||
|
|
||||||
private val animeFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
||||||
|
|
||||||
private val animePlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
||||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
suspend fun setAnimePlanned() = animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
|
||||||
|
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||||
|
suspend fun setAnimePlanned() =
|
||||||
|
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||||
|
|
||||||
|
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
|
||||||
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
||||||
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
||||||
|
|
||||||
private val mangaFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
||||||
|
|
||||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
||||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
suspend fun setMangaPlanned() = mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
|
||||||
|
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||||
|
suspend fun setMangaPlanned() =
|
||||||
|
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||||
|
|
||||||
|
private val recommendation: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
private val recommendation: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
|
||||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||||
|
|
||||||
@@ -93,7 +118,9 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||||||
var notSet = true
|
var notSet = true
|
||||||
lateinit var searchResults: SearchResults
|
lateinit var searchResults: SearchResults
|
||||||
private val type = "ANIME"
|
private val type = "ANIME"
|
||||||
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
private val trending: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||||
suspend fun loadTrending(i: Int) {
|
suspend fun loadTrending(i: Int) {
|
||||||
val (season, year) = Anilist.currentSeasons[i]
|
val (season, year) = Anilist.currentSeasons[i]
|
||||||
@@ -109,7 +136,9 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
private val updated: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||||
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
||||||
|
|
||||||
@@ -157,15 +186,33 @@ class AnilistMangaViewModel : ViewModel() {
|
|||||||
var notSet = true
|
var notSet = true
|
||||||
lateinit var searchResults: SearchResults
|
lateinit var searchResults: SearchResults
|
||||||
private val type = "MANGA"
|
private val type = "MANGA"
|
||||||
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
private val trending: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||||
suspend fun loadTrending() =
|
suspend fun loadTrending() =
|
||||||
trending.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], hd = true)?.results)
|
trending.postValue(
|
||||||
|
Anilist.query.search(
|
||||||
|
type,
|
||||||
|
perPage = 10,
|
||||||
|
sort = Anilist.sortBy[2],
|
||||||
|
hd = true
|
||||||
|
)?.results
|
||||||
|
)
|
||||||
|
|
||||||
|
private val updated: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
|
||||||
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
|
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
|
||||||
suspend fun loadTrendingNovel() =
|
suspend fun loadTrendingNovel() =
|
||||||
updated.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], format = "NOVEL")?.results)
|
updated.postValue(
|
||||||
|
Anilist.query.search(
|
||||||
|
type,
|
||||||
|
perPage = 10,
|
||||||
|
sort = Anilist.sortBy[2],
|
||||||
|
format = "NOVEL"
|
||||||
|
)?.results
|
||||||
|
)
|
||||||
|
|
||||||
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
||||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||||
|
|||||||
@@ -6,15 +6,20 @@ import android.os.Bundle
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.logger
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
val data: Uri? = intent?.data
|
val data: Uri? = intent?.data
|
||||||
logger(data.toString())
|
logger(data.toString())
|
||||||
try {
|
try {
|
||||||
Anilist.token = Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
Anilist.token =
|
||||||
|
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||||
val filename = "anilistToken"
|
val filename = "anilistToken"
|
||||||
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
|
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
|
||||||
it.write(Anilist.token!!.toByteArray())
|
it.write(Anilist.token!!.toByteArray())
|
||||||
|
|||||||
@@ -27,7 +27,15 @@ data class SearchResults(
|
|||||||
val list = mutableListOf<SearchChip>()
|
val list = mutableListOf<SearchChip>()
|
||||||
sort?.let {
|
sort?.let {
|
||||||
val c = currContext()!!
|
val c = currContext()!!
|
||||||
list.add(SearchChip("SORT", c.getString(R.string.filter_sort, c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)])))
|
list.add(
|
||||||
|
SearchChip(
|
||||||
|
"SORT",
|
||||||
|
c.getString(
|
||||||
|
R.string.filter_sort,
|
||||||
|
c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
format?.let {
|
format?.let {
|
||||||
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
||||||
@@ -42,13 +50,23 @@ data class SearchResults(
|
|||||||
list.add(SearchChip("GENRE", it))
|
list.add(SearchChip("GENRE", it))
|
||||||
}
|
}
|
||||||
excludedGenres?.forEach {
|
excludedGenres?.forEach {
|
||||||
list.add(SearchChip("EXCLUDED_GENRE", currContext()!!.getString(R.string.filter_exclude, it)))
|
list.add(
|
||||||
|
SearchChip(
|
||||||
|
"EXCLUDED_GENRE",
|
||||||
|
currContext()!!.getString(R.string.filter_exclude, it)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
tags?.forEach {
|
tags?.forEach {
|
||||||
list.add(SearchChip("TAG", it))
|
list.add(SearchChip("TAG", it))
|
||||||
}
|
}
|
||||||
excludedTags?.forEach {
|
excludedTags?.forEach {
|
||||||
list.add(SearchChip("EXCLUDED_TAG", currContext()!!.getString(R.string.filter_exclude, it)))
|
list.add(
|
||||||
|
SearchChip(
|
||||||
|
"EXCLUDED_TAG",
|
||||||
|
currContext()!!.getString(R.string.filter_exclude, it)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import ani.dantotsu.loadMedia
|
import ani.dantotsu.loadMedia
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class UrlMedia : Activity() {
|
class UrlMedia : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||||
var isMAL = false
|
var isMAL = false
|
||||||
var continueMedia = true
|
var continueMedia = true
|
||||||
@@ -19,6 +23,9 @@ class UrlMedia : Activity() {
|
|||||||
isMAL = data?.host != "anilist.co"
|
isMAL = data?.host != "anilist.co"
|
||||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||||
} else loadMedia = id
|
} else loadMedia = id
|
||||||
startMainActivity(this, bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia))
|
startMainActivity(
|
||||||
|
this,
|
||||||
|
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ class Query{
|
|||||||
val user: ani.dantotsu.connections.anilist.api.User?
|
val user: ani.dantotsu.connections.anilist.api.User?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Media(
|
data class Media(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package ani.dantotsu.connections.anilist.api
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.text.DateFormatSymbols
|
import java.text.DateFormatSymbols
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class FuzzyDate(
|
data class FuzzyDate(
|
||||||
@@ -16,9 +16,11 @@ data class FuzzyDate(
|
|||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
return year == null && month == null && day == null
|
return year == null && month == null && day == null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return if (isEmpty()) "??" else toStringOrEmpty()
|
return if (isEmpty()) "??" else toStringOrEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toStringOrEmpty(): String {
|
fun toStringOrEmpty(): String {
|
||||||
return listOfNotNull(
|
return listOfNotNull(
|
||||||
day?.toString(),
|
day?.toString(),
|
||||||
@@ -29,7 +31,11 @@ data class FuzzyDate(
|
|||||||
|
|
||||||
fun getToday(): FuzzyDate {
|
fun getToday(): FuzzyDate {
|
||||||
val cal = Calendar.getInstance()
|
val cal = Calendar.getInstance()
|
||||||
return FuzzyDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH))
|
return FuzzyDate(
|
||||||
|
cal.get(Calendar.YEAR),
|
||||||
|
cal.get(Calendar.MONTH) + 1,
|
||||||
|
cal.get(Calendar.DAY_OF_MONTH)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toVariableString(): String {
|
fun toVariableString(): String {
|
||||||
@@ -39,6 +45,7 @@ data class FuzzyDate(
|
|||||||
day?.let { "day:$it" }
|
day?.let { "day:$it" }
|
||||||
).joinToString(",", "{", "}")
|
).joinToString(",", "{", "}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toMALString(): String {
|
fun toMALString(): String {
|
||||||
val padding = '0'
|
val padding = '0'
|
||||||
val values = listOf(
|
val values = listOf(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ani.dantotsu.connections.anilist.api
|
|||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Recommendation(
|
data class Recommendation(
|
||||||
// The id of the recommendation
|
// The id of the recommendation
|
||||||
@@ -22,6 +23,7 @@ data class Recommendation(
|
|||||||
// The user that first created the recommendation
|
// The user that first created the recommendation
|
||||||
@SerialName("user") var user: User?,
|
@SerialName("user") var user: User?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RecommendationConnection(
|
data class RecommendationConnection(
|
||||||
//@SerialName("edges") var edges: List<RecommendationEdge>?,
|
//@SerialName("edges") var edges: List<RecommendationEdge>?,
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ import android.content.Intent
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.discord.serializers.User
|
|
||||||
import ani.dantotsu.others.CustomBottomDialog
|
import ani.dantotsu.others.CustomBottomDialog
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
import ani.dantotsu.tryWith
|
import ani.dantotsu.tryWith
|
||||||
import ani.dantotsu.tryWithSuspend
|
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Discord {
|
object Discord {
|
||||||
@@ -21,7 +18,7 @@ object Discord {
|
|||||||
var userid: String? = null
|
var userid: String? = null
|
||||||
var avatar: String? = null
|
var avatar: String? = null
|
||||||
|
|
||||||
private const val TOKEN = "discord_token"
|
const val TOKEN = "discord_token"
|
||||||
|
|
||||||
fun getSavedToken(context: Context): Boolean {
|
fun getSavedToken(context: Context): Boolean {
|
||||||
val sharedPref = context.getSharedPreferences(
|
val sharedPref = context.getSharedPreferences(
|
||||||
@@ -61,16 +58,6 @@ object Discord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var rpc: RPC? = null
|
private var rpc: RPC? = null
|
||||||
suspend fun getUserData() = tryWithSuspend(true) {
|
|
||||||
if(rpc==null) {
|
|
||||||
val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it }
|
|
||||||
val user: User = rpc.getUserData()
|
|
||||||
userid = user.username
|
|
||||||
avatar = user.userAvatar()
|
|
||||||
rpc.close()
|
|
||||||
true
|
|
||||||
} else true
|
|
||||||
} ?: false
|
|
||||||
|
|
||||||
|
|
||||||
fun warning(context: Context) = CustomBottomDialog().apply {
|
fun warning(context: Context) = CustomBottomDialog().apply {
|
||||||
@@ -97,16 +84,21 @@ object Discord {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultRPC(): RPC? {
|
const val application_Id = "1163925779692912771"
|
||||||
|
const val small_Image: String =
|
||||||
|
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
|
||||||
|
/*fun defaultRPC(): RPC? {
|
||||||
return token?.let {
|
return token?.let {
|
||||||
RPC(it, Dispatchers.IO).apply {
|
RPC(it, Dispatchers.IO).apply {
|
||||||
applicationId = "1163925779692912771"
|
applicationId = application_Id
|
||||||
smallImage = RPC.Link(
|
smallImage = RPC.Link(
|
||||||
"Dantotsu",
|
"Dantotsu",
|
||||||
"mp:attachments/1163940221063278672/1163940262423298141/bitmap1024.png"
|
small_Image
|
||||||
)
|
)
|
||||||
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
|
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
package ani.dantotsu.connections.discord
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import ani.dantotsu.MainActivity
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.connections.discord.serializers.Presence
|
||||||
|
import ani.dantotsu.connections.discord.serializers.User
|
||||||
|
import ani.dantotsu.isOnline
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.WebSocket
|
||||||
|
import okhttp3.WebSocketListener
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
|
||||||
|
class DiscordService : Service() {
|
||||||
|
private var heartbeat: Int = 0
|
||||||
|
private var sequence: Int? = null
|
||||||
|
private var sessionId: String = ""
|
||||||
|
private var resume = false
|
||||||
|
private lateinit var logFile: File
|
||||||
|
private lateinit var webSocket: WebSocket
|
||||||
|
private lateinit var heartbeatThread: Thread
|
||||||
|
private lateinit var client: OkHttpClient
|
||||||
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
var presenceStore = ""
|
||||||
|
val json = Json {
|
||||||
|
encodeDefaults = true
|
||||||
|
allowStructuredMapKeys = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
coerceInputValues = true
|
||||||
|
}
|
||||||
|
var log = ""
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
log("Service onCreate()")
|
||||||
|
val powerManager = baseContext.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
wakeLock = powerManager.newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
|
"discordRPC:backgroundPresence"
|
||||||
|
)
|
||||||
|
wakeLock.acquire()
|
||||||
|
log("WakeLock Acquired")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val serviceChannel = NotificationChannel(
|
||||||
|
"discordPresence",
|
||||||
|
"Discord Presence Service Channel",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.createNotificationChannel(serviceChannel)
|
||||||
|
}
|
||||||
|
val intent = Intent(this, MainActivity::class.java).apply {
|
||||||
|
action = Intent.ACTION_MAIN
|
||||||
|
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
val pendingIntent =
|
||||||
|
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val builder = NotificationCompat.Builder(this, "discordPresence")
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher_round)
|
||||||
|
.setContentTitle("Discord Presence")
|
||||||
|
.setContentText("Running in the background")
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
startForeground(1, builder.build())
|
||||||
|
log("Foreground service started, notification shown")
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
SERVICE_RUNNING = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
log("Service onStartCommand()")
|
||||||
|
if (intent != null) {
|
||||||
|
if (intent.hasExtra("presence")) {
|
||||||
|
log("Service onStartCommand() setPresence")
|
||||||
|
val lPresence = intent.getStringExtra("presence")
|
||||||
|
if (this::webSocket.isInitialized) webSocket.send(lPresence!!)
|
||||||
|
presenceStore = lPresence!!
|
||||||
|
} else {
|
||||||
|
log("Service onStartCommand() no presence")
|
||||||
|
DiscordServiceRunningSingleton.running = false
|
||||||
|
//kill the client
|
||||||
|
client = OkHttpClient()
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_REDELIVER_INTENT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
log("Service Destroyed")
|
||||||
|
if (DiscordServiceRunningSingleton.running) {
|
||||||
|
log("Accidental Service Destruction, restarting service")
|
||||||
|
val intent = Intent(baseContext, DiscordService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
baseContext.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
baseContext.startService(intent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this::webSocket.isInitialized)
|
||||||
|
setPresence(
|
||||||
|
json.encodeToString(
|
||||||
|
Presence.Response(
|
||||||
|
3,
|
||||||
|
Presence(status = "offline")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
|
SERVICE_RUNNING = false
|
||||||
|
client = OkHttpClient()
|
||||||
|
if (this::webSocket.isInitialized) webSocket.close(1000, "Closed by user")
|
||||||
|
super.onDestroy()
|
||||||
|
//saveLogToFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveProfile(response: String) {
|
||||||
|
val sharedPref = baseContext.getSharedPreferences(
|
||||||
|
baseContext.getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
val user = json.decodeFromString<User.Response>(response).d.user
|
||||||
|
log("User data: $user")
|
||||||
|
with(sharedPref.edit()) {
|
||||||
|
putString("discord_username", user.username)
|
||||||
|
putString("discord_id", user.id)
|
||||||
|
putString("discord_avatar", user.avatar)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(p0: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||||
|
|
||||||
|
var retryAttempts = 0
|
||||||
|
val maxRetryAttempts = 10
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
super.onOpen(webSocket, response)
|
||||||
|
this@DiscordService.webSocket = webSocket
|
||||||
|
log("WebSocket: Opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
super.onMessage(webSocket, text)
|
||||||
|
val json = JsonParser.parseString(text).asJsonObject
|
||||||
|
log("WebSocket: Received op code ${json.get("op")}")
|
||||||
|
when (json.get("op").asInt) {
|
||||||
|
0 -> {
|
||||||
|
if (json.has("s")) {
|
||||||
|
log("WebSocket: Sequence ${json.get("s")} Received")
|
||||||
|
sequence = json.get("s").asInt
|
||||||
|
}
|
||||||
|
if (json.get("t").asString != "READY") return
|
||||||
|
saveProfile(text)
|
||||||
|
log(text)
|
||||||
|
sessionId = json.get("d").asJsonObject.get("session_id").asString
|
||||||
|
log("WebSocket: SessionID ${json.get("d").asJsonObject.get("session_id")} Received")
|
||||||
|
if (presenceStore.isNotEmpty()) setPresence(presenceStore)
|
||||||
|
sendBroadcast(Intent("ServiceToConnectButton"))
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
log("WebSocket: Received Heartbeat request, sending heartbeat")
|
||||||
|
heartbeatThread.interrupt()
|
||||||
|
heartbeatSend(webSocket, sequence)
|
||||||
|
heartbeatThread = Thread(HeartbeatRunnable())
|
||||||
|
heartbeatThread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
7 -> {
|
||||||
|
resume = true
|
||||||
|
log("WebSocket: Requested to Restart, restarting")
|
||||||
|
webSocket.close(1000, "Requested to Restart by the server")
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json")
|
||||||
|
.build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
9 -> {
|
||||||
|
log("WebSocket: Invalid Session, restarting")
|
||||||
|
webSocket.close(1000, "Invalid Session")
|
||||||
|
Thread.sleep(5000)
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json")
|
||||||
|
.build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
10 -> {
|
||||||
|
heartbeat = json.get("d").asJsonObject.get("heartbeat_interval").asInt
|
||||||
|
heartbeatThread = Thread(HeartbeatRunnable())
|
||||||
|
heartbeatThread.start()
|
||||||
|
if (resume) {
|
||||||
|
log("WebSocket: Resuming because server requested")
|
||||||
|
resume()
|
||||||
|
resume = false
|
||||||
|
} else {
|
||||||
|
identify(webSocket, baseContext)
|
||||||
|
log("WebSocket: Identified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
11 -> {
|
||||||
|
log("WebSocket: Heartbeat ACKed")
|
||||||
|
heartbeatThread = Thread(HeartbeatRunnable())
|
||||||
|
heartbeatThread.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun identify(webSocket: WebSocket, context: Context) {
|
||||||
|
val properties = JsonObject()
|
||||||
|
properties.addProperty("os", "linux")
|
||||||
|
properties.addProperty("browser", "unknown")
|
||||||
|
properties.addProperty("device", "unknown")
|
||||||
|
val d = JsonObject()
|
||||||
|
d.addProperty("token", getToken(context))
|
||||||
|
d.addProperty("intents", 0)
|
||||||
|
d.add("properties", properties)
|
||||||
|
val payload = JsonObject()
|
||||||
|
payload.addProperty("op", 2)
|
||||||
|
payload.add("d", d)
|
||||||
|
webSocket.send(payload.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
|
super.onFailure(webSocket, t, response)
|
||||||
|
if (!isOnline(baseContext)) {
|
||||||
|
log("WebSocket: Error, onFailure() reason: No Internet")
|
||||||
|
errorNotification("Could not set the presence", "No Internet")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
retryAttempts++
|
||||||
|
if (retryAttempts >= maxRetryAttempts) {
|
||||||
|
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
|
||||||
|
errorNotification("Could not set the presence", "Max Retry Attempts")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
|
||||||
|
log("WebSocket: Error, onFailure() reason: ${t.message}")
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||||
|
heartbeatThread.interrupt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
super.onClosing(webSocket, code, reason)
|
||||||
|
Log.d("WebSocket", "onClosing() $code $reason")
|
||||||
|
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||||
|
heartbeatThread.interrupt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
super.onClosed(webSocket, code, reason)
|
||||||
|
Log.d("WebSocket", "onClosed() $code $reason")
|
||||||
|
if (code >= 4000) {
|
||||||
|
log("WebSocket: Error, code: $code reason: $reason")
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getToken(context: Context): String {
|
||||||
|
val sharedPref = context.getSharedPreferences(
|
||||||
|
context.getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
val token = sharedPref.getString(Discord.TOKEN, null)
|
||||||
|
if (token == null) {
|
||||||
|
log("WebSocket: Token not found")
|
||||||
|
errorNotification("Could not set the presence", "token not found")
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun heartbeatSend(webSocket: WebSocket, seq: Int?) {
|
||||||
|
val json = JsonObject()
|
||||||
|
json.addProperty("op", 1)
|
||||||
|
json.addProperty("d", seq)
|
||||||
|
webSocket.send(json.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun errorNotification(title: String, text: String) {
|
||||||
|
val intent = Intent(this@DiscordService, MainActivity::class.java).apply {
|
||||||
|
action = Intent.ACTION_MAIN
|
||||||
|
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
val pendingIntent =
|
||||||
|
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val builder = NotificationCompat.Builder(this@DiscordService, "discordPresence")
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher_round)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
//TODO: Request permission
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notificationManager.notify(2, builder.build())
|
||||||
|
log("Error Notified")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveSimpleTestPresence() {
|
||||||
|
val file = File(baseContext.cacheDir, "payload")
|
||||||
|
//fill with test payload
|
||||||
|
val payload = JsonObject()
|
||||||
|
payload.addProperty("op", 3)
|
||||||
|
payload.add("d", JsonObject().apply {
|
||||||
|
addProperty("status", "online")
|
||||||
|
addProperty("afk", false)
|
||||||
|
add("activities", JsonArray().apply {
|
||||||
|
add(JsonObject().apply {
|
||||||
|
addProperty("name", "Test")
|
||||||
|
addProperty("type", 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
file.writeText(payload.toString())
|
||||||
|
log("WebSocket: Simple Test Presence Saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPresence(String: String) {
|
||||||
|
log("WebSocket: Sending Presence payload")
|
||||||
|
log(String)
|
||||||
|
webSocket.send(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun log(string: String) {
|
||||||
|
Log.d("WebSocket_Discord", string)
|
||||||
|
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLogToFile() {
|
||||||
|
val fileName = "log_${System.currentTimeMillis()}.txt"
|
||||||
|
|
||||||
|
// ContentValues to store file metadata
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserting the file in the MediaStore
|
||||||
|
val resolver = baseContext.contentResolver
|
||||||
|
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||||
|
} else {
|
||||||
|
val directory =
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
val file = File(directory, fileName)
|
||||||
|
|
||||||
|
// Make sure the Downloads directory exists
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use FileProvider to get the URI for the file
|
||||||
|
val authority =
|
||||||
|
"${baseContext.packageName}.provider" // Adjust with your app's package name
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the file
|
||||||
|
uri?.let {
|
||||||
|
resolver.openOutputStream(it).use { outputStream ->
|
||||||
|
OutputStreamWriter(outputStream).use { writer ->
|
||||||
|
writer.write(log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
log("Error saving log file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resume() {
|
||||||
|
log("Sending Resume payload")
|
||||||
|
val d = JsonObject()
|
||||||
|
d.addProperty("token", getToken(baseContext))
|
||||||
|
d.addProperty("session_id", sessionId)
|
||||||
|
d.addProperty("seq", sequence)
|
||||||
|
val json = JsonObject()
|
||||||
|
json.addProperty("op", 6)
|
||||||
|
json.add("d", d)
|
||||||
|
log(json.toString())
|
||||||
|
webSocket.send(json.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class HeartbeatRunnable : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(heartbeat.toLong())
|
||||||
|
heartbeatSend(webSocket, sequence)
|
||||||
|
log("WebSocket: Heartbeat Sent")
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var SERVICE_RUNNING = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object DiscordServiceRunningSingleton {
|
||||||
|
var running = false
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,19 +4,24 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Application.getProcessName
|
import android.app.Application.getProcessName
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.annotation.RequiresApi
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
val process = getProcessName()
|
val process = getProcessName()
|
||||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||||
@@ -30,28 +35,46 @@ class Login : AppCompatActivity() {
|
|||||||
settings.databaseEnabled = true
|
settings.databaseEnabled = true
|
||||||
settings.domStorageEnabled = true
|
settings.domStorageEnabled = true
|
||||||
}
|
}
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true)
|
||||||
webView.webViewClient = object : WebViewClient() {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
override fun shouldOverrideUrlLoading(
|
||||||
if (url != null && url.endsWith("/app")) {
|
view: WebView?,
|
||||||
webView.stopLoading()
|
request: WebResourceRequest?
|
||||||
webView.evaluateJavascript("""
|
): Boolean {
|
||||||
|
// Check if the URL is the one expected after a successful login
|
||||||
|
if (request?.url.toString() != "https://discord.com/login") {
|
||||||
|
// Delay the script execution to ensure the page is fully loaded
|
||||||
|
view?.postDelayed({
|
||||||
|
view.evaluateJavascript(
|
||||||
|
"""
|
||||||
(function() {
|
(function() {
|
||||||
const wreq = webpackChunkdiscord_app.push([[Symbol()], {}, w => w])
|
const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken();
|
||||||
webpackChunkdiscord_app.pop()
|
return wreq;
|
||||||
const token = Object.values(wreq.c).find(m => m.exports?.Z?.getToken).exports.Z.getToken();
|
|
||||||
return token;
|
|
||||||
})()
|
})()
|
||||||
""".trimIndent()){
|
""".trimIndent()
|
||||||
login(it.trim('"'))
|
) { result ->
|
||||||
}
|
login(result.trim('"'))
|
||||||
}
|
}
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
return super.shouldOverrideUrlLoading(view, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webView.loadUrl("https://discord.com/login")
|
webView.loadUrl("https://discord.com/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun login(token: String) {
|
private fun login(token: String) {
|
||||||
|
if (token.isEmpty() || token == "null") {
|
||||||
|
Toast.makeText(this, "Failed to retrieve token", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
||||||
finish()
|
finish()
|
||||||
saveToken(this, token)
|
saveToken(this, token)
|
||||||
startMainActivity(this@Login)
|
startMainActivity(this@Login)
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
package ani.dantotsu.connections.discord
|
package ani.dantotsu.connections.discord
|
||||||
|
|
||||||
import ani.dantotsu.connections.discord.serializers.*
|
import ani.dantotsu.connections.discord.serializers.Activity
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import ani.dantotsu.connections.discord.serializers.Presence
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.WebSocket
|
|
||||||
import okhttp3.WebSocketListener
|
|
||||||
import java.util.concurrent.TimeUnit.*
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import ani.dantotsu.client as app
|
import ani.dantotsu.client as app
|
||||||
|
|
||||||
@@ -31,72 +17,30 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
|||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(10, SECONDS)
|
|
||||||
.readTimeout(10, SECONDS)
|
|
||||||
.writeTimeout(10, SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val request = Request.Builder()
|
|
||||||
.url("wss://gateway.discord.gg/?encoding=json&v=10")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private var webSocket = client.newWebSocket(request, Listener())
|
|
||||||
|
|
||||||
var applicationId: String? = null
|
|
||||||
var type: Type? = null
|
|
||||||
var activityName: String? = null
|
|
||||||
var details: String? = null
|
|
||||||
var state: String? = null
|
|
||||||
var largeImage: Link? = null
|
|
||||||
var smallImage: Link? = null
|
|
||||||
var status: String? = null
|
|
||||||
var startTimestamp: Long? = null
|
|
||||||
var stopTimestamp: Long? = null
|
|
||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
|
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
|
||||||
}
|
}
|
||||||
|
|
||||||
var buttons = mutableListOf<Link>()
|
|
||||||
|
|
||||||
data class Link(val label: String, val url: String)
|
data class Link(val label: String, val url: String)
|
||||||
|
|
||||||
private suspend fun createPresence(): String {
|
companion object {
|
||||||
return json.encodeToString(Presence.Response(
|
data class RPCData(
|
||||||
3,
|
val applicationId: String? = null,
|
||||||
Presence(
|
val type: Type? = null,
|
||||||
activities = listOf(
|
val activityName: String? = null,
|
||||||
Activity(
|
val details: String? = null,
|
||||||
name = activityName,
|
val state: String? = null,
|
||||||
state = state,
|
val largeImage: Link? = null,
|
||||||
details = details,
|
val smallImage: Link? = null,
|
||||||
type = type?.ordinal,
|
val status: String? = null,
|
||||||
timestamps = if (startTimestamp != null)
|
val startTimestamp: Long? = null,
|
||||||
Activity.Timestamps(startTimestamp, stopTimestamp)
|
val stopTimestamp: Long? = null,
|
||||||
else null,
|
val buttons: MutableList<Link> = mutableListOf()
|
||||||
assets = Activity.Assets(
|
|
||||||
largeImage = largeImage?.url?.discordUrl(),
|
|
||||||
largeText = largeImage?.label,
|
|
||||||
smallImage = smallImage?.url?.discordUrl(),
|
|
||||||
smallText = smallImage?.label
|
|
||||||
),
|
|
||||||
buttons = buttons.map { it.label },
|
|
||||||
metadata = Activity.Metadata(
|
|
||||||
buttonUrls = buttons.map { it.url }
|
|
||||||
),
|
|
||||||
applicationId = applicationId,
|
|
||||||
)
|
)
|
||||||
),
|
|
||||||
afk = true,
|
|
||||||
since = startTimestamp,
|
|
||||||
status = status
|
|
||||||
)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class KizzyApi(val id: String)
|
data class KizzyApi(val id: String)
|
||||||
|
|
||||||
val api = "https://kizzy-api.vercel.app/image?url="
|
val api = "https://kizzy-api.vercel.app/image?url="
|
||||||
private suspend fun String.discordUrl(): String? {
|
private suspend fun String.discordUrl(): String? {
|
||||||
if (startsWith("mp:")) return this
|
if (startsWith("mp:")) return this
|
||||||
@@ -104,132 +48,42 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
|||||||
return json?.id
|
return json?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendIdentify() {
|
suspend fun createPresence(data: RPCData): String {
|
||||||
val response = Identity.Response(
|
val json = Json {
|
||||||
op = 2,
|
encodeDefaults = true
|
||||||
d = Identity(
|
allowStructuredMapKeys = true
|
||||||
token = token,
|
ignoreUnknownKeys = true
|
||||||
properties = Identity.Properties(
|
|
||||||
os = "windows",
|
|
||||||
browser = "Chrome",
|
|
||||||
device = "disco"
|
|
||||||
),
|
|
||||||
compress = false,
|
|
||||||
intents = 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
webSocket.send(json.encodeToString(response))
|
|
||||||
}
|
}
|
||||||
|
return json.encodeToString(Presence.Response(
|
||||||
fun send(block: RPC.() -> Unit) {
|
|
||||||
block.invoke(this)
|
|
||||||
send()
|
|
||||||
}
|
|
||||||
|
|
||||||
var started = false
|
|
||||||
var whenStarted: ((User) -> Unit)? = null
|
|
||||||
|
|
||||||
fun send() {
|
|
||||||
val send = {
|
|
||||||
CoroutineScope(coroutineContext).launch {
|
|
||||||
webSocket.send(createPresence())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!started) whenStarted = {
|
|
||||||
send.invoke()
|
|
||||||
whenStarted = null
|
|
||||||
}
|
|
||||||
else send.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
webSocket.send(
|
|
||||||
json.encodeToString(
|
|
||||||
Presence.Response(
|
|
||||||
3,
|
3,
|
||||||
Presence(status = "offline")
|
Presence(
|
||||||
|
activities = listOf(
|
||||||
|
Activity(
|
||||||
|
name = data.activityName,
|
||||||
|
state = data.state,
|
||||||
|
details = data.details,
|
||||||
|
type = data.type?.ordinal,
|
||||||
|
timestamps = if (data.startTimestamp != null)
|
||||||
|
Activity.Timestamps(data.startTimestamp, data.stopTimestamp)
|
||||||
|
else null,
|
||||||
|
assets = Activity.Assets(
|
||||||
|
largeImage = data.largeImage?.url?.discordUrl(),
|
||||||
|
largeText = data.largeImage?.label,
|
||||||
|
smallImage = data.smallImage?.url?.discordUrl(),
|
||||||
|
smallText = data.smallImage?.label
|
||||||
|
),
|
||||||
|
buttons = data.buttons.map { it.label },
|
||||||
|
metadata = Activity.Metadata(
|
||||||
|
buttonUrls = data.buttons.map { it.url }
|
||||||
|
),
|
||||||
|
applicationId = data.applicationId,
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
afk = true,
|
||||||
|
since = data.startTimestamp,
|
||||||
|
status = data.status
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
webSocket.close(4000, "Interrupt")
|
|
||||||
}
|
|
||||||
|
|
||||||
//I hate this, but couldn't find any better way to solve it
|
|
||||||
suspend fun getUserData(): User {
|
|
||||||
var user : User? = null
|
|
||||||
whenStarted = {
|
|
||||||
user = it
|
|
||||||
whenStarted = null
|
|
||||||
}
|
|
||||||
while (user == null) {
|
|
||||||
delay(100)
|
|
||||||
}
|
|
||||||
return user!!
|
|
||||||
}
|
|
||||||
|
|
||||||
var onReceiveUserData: ((User) -> Deferred<Unit>)? = null
|
|
||||||
|
|
||||||
inner class Listener : WebSocketListener() {
|
|
||||||
private var seq: Int? = null
|
|
||||||
private var heartbeatInterval: Long? = null
|
|
||||||
|
|
||||||
var scope = CoroutineScope(coroutineContext)
|
|
||||||
|
|
||||||
private fun sendHeartBeat() {
|
|
||||||
scope.cancel()
|
|
||||||
scope = CoroutineScope(coroutineContext)
|
|
||||||
scope.launch {
|
|
||||||
delay(heartbeatInterval!!)
|
|
||||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
|
||||||
println("Message : $text")
|
|
||||||
|
|
||||||
val map = json.decodeFromString<Res>(text)
|
|
||||||
seq = map.s
|
|
||||||
|
|
||||||
when (map.op) {
|
|
||||||
10 -> {
|
|
||||||
map.d as JsonObject
|
|
||||||
heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long
|
|
||||||
sendHeartBeat()
|
|
||||||
sendIdentify()
|
|
||||||
}
|
|
||||||
|
|
||||||
0 -> if (map.t == "READY") {
|
|
||||||
val user = json.decodeFromString<User.Response>(text).d.user
|
|
||||||
started = true
|
|
||||||
whenStarted?.invoke(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
if (scope.isActive) scope.cancel()
|
|
||||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
|
||||||
}
|
|
||||||
|
|
||||||
11 -> sendHeartBeat()
|
|
||||||
7 -> webSocket.close(400, "Reconnect")
|
|
||||||
9 -> {
|
|
||||||
sendHeartBeat()
|
|
||||||
sendIdentify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
|
||||||
println("Server Closed : $code $reason")
|
|
||||||
if (code == 4000) {
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
|
||||||
println("Failure : ${t.message}")
|
|
||||||
if (t.message != "Interrupt") {
|
|
||||||
this@RPC.webSocket = client.newWebSocket(request, Listener())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ani.dantotsu.connections.discord.serializers
|
|||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Activity(
|
data class Activity(
|
||||||
@SerialName("application_id")
|
@SerialName("application_id")
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
package ani.dantotsu.connections.discord.serializers
|
package ani.dantotsu.connections.discord.serializers
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.descriptors.*
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encoding.*
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.*
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class User(
|
data class User(
|
||||||
val verified: Boolean,
|
val verified: Boolean? = null,
|
||||||
val username: String,
|
val username: String,
|
||||||
|
|
||||||
@SerialName("purchased_flags")
|
@SerialName("purchased_flags")
|
||||||
val purchasedFlags: Long,
|
val purchasedFlags: Long? = null,
|
||||||
|
|
||||||
@SerialName("public_flags")
|
@SerialName("public_flags")
|
||||||
val publicFlags: Long,
|
val publicFlags: Long? = null,
|
||||||
|
|
||||||
val pronouns: String,
|
val pronouns: String? = null,
|
||||||
|
|
||||||
@SerialName("premium_type")
|
@SerialName("premium_type")
|
||||||
val premiumType: Long,
|
val premiumType: Long? = null,
|
||||||
|
|
||||||
val premium: Boolean,
|
val premium: Boolean? = null,
|
||||||
val phone: String,
|
val phone: String? = null,
|
||||||
|
|
||||||
@SerialName("nsfw_allowed")
|
@SerialName("nsfw_allowed")
|
||||||
val nsfwAllowed: Boolean,
|
val nsfwAllowed: Boolean? = null,
|
||||||
|
|
||||||
val mobile: Boolean,
|
val mobile: Boolean? = null,
|
||||||
|
|
||||||
@SerialName("mfa_enabled")
|
@SerialName("mfa_enabled")
|
||||||
val mfaEnabled: Boolean,
|
val mfaEnabled: Boolean? = null,
|
||||||
|
|
||||||
val id: String,
|
val id: String,
|
||||||
|
|
||||||
@SerialName("global_name")
|
@SerialName("global_name")
|
||||||
val globalName: String,
|
val globalName: String? = null,
|
||||||
|
|
||||||
val flags: Long,
|
val flags: Long? = null,
|
||||||
val email: String,
|
val email: String? = null,
|
||||||
val discriminator: String,
|
val discriminator: String? = null,
|
||||||
val desktop: Boolean,
|
val desktop: Boolean? = null,
|
||||||
val bio: String,
|
val bio: String? = null,
|
||||||
|
|
||||||
@SerialName("banner_color")
|
@SerialName("banner_color")
|
||||||
val bannerColor: String,
|
val bannerColor: String? = null,
|
||||||
|
|
||||||
val banner: JsonElement? = null,
|
val banner: JsonElement? = null,
|
||||||
|
|
||||||
@SerialName("avatar_decoration")
|
@SerialName("avatar_decoration")
|
||||||
val avatarDecoration: JsonElement? = null,
|
val avatarDecoration: JsonElement? = null,
|
||||||
|
|
||||||
val avatar: String,
|
val avatar: String? = null,
|
||||||
|
|
||||||
@SerialName("accent_color")
|
@SerialName("accent_color")
|
||||||
val accentColor: Long
|
val accentColor: Long? = null
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Response(
|
data class Response(
|
||||||
|
|||||||
@@ -4,15 +4,25 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.client
|
||||||
import ani.dantotsu.connections.mal.MAL.clientId
|
import ani.dantotsu.connections.mal.MAL.clientId
|
||||||
import ani.dantotsu.connections.mal.MAL.saveResponse
|
import ani.dantotsu.connections.mal.MAL.saveResponse
|
||||||
|
import ani.dantotsu.loadData
|
||||||
|
import ani.dantotsu.logError
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.startMainActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import ani.dantotsu.tryWithSuspend
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
try {
|
try {
|
||||||
val data: Uri = intent?.data
|
val data: Uri = intent?.data
|
||||||
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
||||||
@@ -42,8 +52,7 @@ class Login : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
catch (e:Exception){
|
|
||||||
logError(e, snackbar = false)
|
logError(e, snackbar = false)
|
||||||
startMainActivity(this)
|
startMainActivity(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import android.net.Uri
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.loadData
|
||||||
|
import ani.dantotsu.openLinkInBrowser
|
||||||
|
import ani.dantotsu.saveData
|
||||||
|
import ani.dantotsu.tryWithSuspend
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package ani.dantotsu.connections.mal
|
package ani.dantotsu.connections.mal
|
||||||
|
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
|
||||||
import ani.dantotsu.client
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|||||||
304
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
304
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
package ani.dantotsu.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Environment
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import java.io.File
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
class DownloadsManager(private val context: Context) {
|
||||||
|
private val prefs: SharedPreferences =
|
||||||
|
context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
|
||||||
|
private val gson = Gson()
|
||||||
|
private val downloadsList = loadDownloads().toMutableList()
|
||||||
|
|
||||||
|
val mangaDownloadedTypes: List<DownloadedType>
|
||||||
|
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
||||||
|
val animeDownloadedTypes: List<DownloadedType>
|
||||||
|
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
||||||
|
val novelDownloadedTypes: List<DownloadedType>
|
||||||
|
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
||||||
|
|
||||||
|
private fun saveDownloads() {
|
||||||
|
val jsonString = gson.toJson(downloadsList)
|
||||||
|
prefs.edit().putString("downloads_key", jsonString).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDownloads(): List<DownloadedType> {
|
||||||
|
val jsonString = prefs.getString("downloads_key", null)
|
||||||
|
return if (jsonString != null) {
|
||||||
|
val type = object : TypeToken<List<DownloadedType>>() {}.type
|
||||||
|
gson.fromJson(jsonString, type)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addDownload(downloadedType: DownloadedType) {
|
||||||
|
downloadsList.add(downloadedType)
|
||||||
|
saveDownloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeDownload(downloadedType: DownloadedType) {
|
||||||
|
downloadsList.remove(downloadedType)
|
||||||
|
removeDirectory(downloadedType)
|
||||||
|
saveDownloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||||
|
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||||
|
"Manga"
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
"Anime"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$subDirectory/$title"
|
||||||
|
)
|
||||||
|
if (directory.exists()) {
|
||||||
|
val deleted = directory.deleteRecursively()
|
||||||
|
if (deleted) {
|
||||||
|
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
|
cleanDownloads()
|
||||||
|
}
|
||||||
|
when (type) {
|
||||||
|
DownloadedType.Type.MANGA -> {
|
||||||
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
||||||
|
}
|
||||||
|
DownloadedType.Type.ANIME -> {
|
||||||
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
||||||
|
}
|
||||||
|
DownloadedType.Type.NOVEL -> {
|
||||||
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveDownloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanDownloads() {
|
||||||
|
cleanDownload(DownloadedType.Type.MANGA)
|
||||||
|
cleanDownload(DownloadedType.Type.ANIME)
|
||||||
|
cleanDownload(DownloadedType.Type.NOVEL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanDownload(type: DownloadedType.Type) {
|
||||||
|
// remove all folders that are not in the downloads list
|
||||||
|
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||||
|
"Manga"
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
"Anime"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$subDirectory"
|
||||||
|
)
|
||||||
|
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
||||||
|
mangaDownloadedTypes
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
animeDownloadedTypes
|
||||||
|
} else {
|
||||||
|
novelDownloadedTypes
|
||||||
|
}
|
||||||
|
if (directory.exists()) {
|
||||||
|
val files = directory.listFiles()
|
||||||
|
if (files != null) {
|
||||||
|
for (file in files) {
|
||||||
|
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||||
|
val deleted = file.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//now remove all downloads that do not have a folder
|
||||||
|
val iterator = downloadsList.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val download = iterator.next()
|
||||||
|
val downloadDir = File(directory, download.title)
|
||||||
|
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
||||||
|
iterator.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||||
|
{
|
||||||
|
val jsonString = gson.toJson(downloadsList)
|
||||||
|
val file = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/downloads.json"
|
||||||
|
)
|
||||||
|
if (file.parentFile?.exists() == false) {
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
}
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.createNewFile()
|
||||||
|
}
|
||||||
|
file.writeText(jsonString)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||||
|
return downloadsList.contains(downloadedType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
||||||
|
return if (type == null) {
|
||||||
|
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||||
|
} else {
|
||||||
|
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||||
|
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the directory exists and delete it recursively
|
||||||
|
if (directory.exists()) {
|
||||||
|
val deleted = directory.deleteRecursively()
|
||||||
|
if (deleted) {
|
||||||
|
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||||
|
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val destination = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
if (directory.exists()) {
|
||||||
|
val copied = directory.copyRecursively(destination, true)
|
||||||
|
if (copied) {
|
||||||
|
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun purgeDownloads(type: DownloadedType.Type) {
|
||||||
|
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||||
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||||
|
} else {
|
||||||
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||||
|
}
|
||||||
|
if (directory.exists()) {
|
||||||
|
val deleted = directory.deleteRecursively()
|
||||||
|
if (deleted) {
|
||||||
|
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadsList.removeAll { it.type == type }
|
||||||
|
saveDownloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val novelLocation = "Dantotsu/Novel"
|
||||||
|
const val mangaLocation = "Dantotsu/Manga"
|
||||||
|
const val animeLocation = "Dantotsu/Anime"
|
||||||
|
|
||||||
|
fun getDirectory(context: Context, type: DownloadedType.Type, title: String, chapter: String? = null): File {
|
||||||
|
return if (type == DownloadedType.Type.MANGA) {
|
||||||
|
if (chapter != null) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$mangaLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$mangaLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
if (chapter != null) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$animeLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$animeLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (chapter != null) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$novelLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$novelLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||||
|
enum class Type {
|
||||||
|
MANGA,
|
||||||
|
ANIME,
|
||||||
|
NOVEL
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
package ani.dantotsu.download.anime
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadManager
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadService
|
||||||
|
import ani.dantotsu.FileUrl
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||||
|
import ani.dantotsu.download.video.Helper
|
||||||
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.SubtitleDownloader
|
||||||
|
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||||
|
import ani.dantotsu.parsers.Subtitle
|
||||||
|
import ani.dantotsu.parsers.Video
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.InstanceCreator
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.Queue
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
class AnimeDownloaderService : Service() {
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
|
private lateinit var builder: NotificationCompat.Builder
|
||||||
|
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
|
private val downloadJobs = mutableMapOf<String, Job>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private var isCurrentlyProcessing = false
|
||||||
|
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
// This is only required for bound services.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
notificationManager = NotificationManagerCompat.from(this)
|
||||||
|
builder =
|
||||||
|
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||||
|
setContentTitle("Anime Download Progress")
|
||||||
|
setSmallIcon(R.drawable.ic_round_download_24)
|
||||||
|
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
builder.build(),
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
this,
|
||||||
|
cancelReceiver,
|
||||||
|
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
AnimeServiceDataSingleton.downloadQueue.clear()
|
||||||
|
downloadJobs.clear()
|
||||||
|
AnimeServiceDataSingleton.isServiceRunning = false
|
||||||
|
unregisterReceiver(cancelReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
snackString("Download started")
|
||||||
|
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
serviceScope.launch {
|
||||||
|
mutex.withLock {
|
||||||
|
if (!isCurrentlyProcessing) {
|
||||||
|
isCurrentlyProcessing = true
|
||||||
|
processQueue()
|
||||||
|
isCurrentlyProcessing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processQueue() {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (AnimeServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||||
|
val task = AnimeServiceDataSingleton.downloadQueue.poll()
|
||||||
|
if (task != null) {
|
||||||
|
val job = launch { download(task) }
|
||||||
|
currentTasks.add(task)
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[task.getTaskName()] = job
|
||||||
|
}
|
||||||
|
job.join() // Wait for the job to complete before continuing to the next task
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs.remove(task.getTaskName())
|
||||||
|
}
|
||||||
|
updateNotification() // Update the notification after each task is completed
|
||||||
|
}
|
||||||
|
if (AnimeServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
stopSelf() // Stop the service when the queue is empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
fun cancelDownload(taskName: String) {
|
||||||
|
val url =
|
||||||
|
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
|
||||||
|
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
snackString("Failed to cancel download")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentTasks.removeAll { it.getTaskName() == taskName }
|
||||||
|
DownloadService.sendSetStopReason(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
ExoplayerDownloadService::class.java,
|
||||||
|
url,
|
||||||
|
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
DownloadService.sendRemoveDownload(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
ExoplayerDownloadService::class.java,
|
||||||
|
url,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[taskName]?.cancel()
|
||||||
|
downloadJobs.remove(taskName)
|
||||||
|
AnimeServiceDataSingleton.downloadQueue.removeAll { it.getTaskName() == taskName }
|
||||||
|
updateNotification() // Update the notification after cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification() {
|
||||||
|
// Update the notification to reflect the current state of the queue
|
||||||
|
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
|
||||||
|
val text = if (pendingDownloads > 0) {
|
||||||
|
"Pending downloads: $pendingDownloads"
|
||||||
|
} else {
|
||||||
|
"All downloads completed"
|
||||||
|
}
|
||||||
|
builder.setContentText(text)
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
|
suspend fun download(task: AnimeDownloadTask) {
|
||||||
|
try {
|
||||||
|
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastDownloadStarted(task.episode)
|
||||||
|
|
||||||
|
currActivity()?.let {
|
||||||
|
Helper.downloadVideo(
|
||||||
|
it,
|
||||||
|
task.video,
|
||||||
|
task.subtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMediaInfo(task)
|
||||||
|
task.subtitle?.let {
|
||||||
|
SubtitleDownloader.downloadSubtitle(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
it.file.url,
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.episode,
|
||||||
|
DownloadedType.Type.ANIME,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val downloadStarted =
|
||||||
|
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
|
||||||
|
|
||||||
|
if (!downloadStarted) {
|
||||||
|
logger("Download failed to start")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download failed to start")
|
||||||
|
broadcastDownloadFailed(task.episode)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// periodically check if the download is complete
|
||||||
|
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
||||||
|
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||||
|
if (download != null) {
|
||||||
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
||||||
|
logger("Download failed")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download failed")
|
||||||
|
logger("Download failed: ${download.failureReason}")
|
||||||
|
downloadsManager.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.episode,
|
||||||
|
DownloadedType.Type.ANIME,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(
|
||||||
|
Exception(
|
||||||
|
"Anime Download failed:" +
|
||||||
|
" ${download.failureReason}" +
|
||||||
|
" url: ${task.video.file.url}" +
|
||||||
|
" title: ${task.title}" +
|
||||||
|
" episode: ${task.episode}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
|
broadcastDownloadFailed(task.episode)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
|
||||||
|
logger("Download completed")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download completed")
|
||||||
|
getSharedPreferences(
|
||||||
|
getString(R.string.anime_downloads),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).edit().putString(
|
||||||
|
task.getTaskName(),
|
||||||
|
task.video.file.url
|
||||||
|
).apply()
|
||||||
|
downloadsManager.addDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.episode,
|
||||||
|
DownloadedType.Type.ANIME,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
|
broadcastDownloadFinished(task.episode)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
|
||||||
|
logger("Download stopped")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download stopped")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download stopped")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
broadcastDownloadProgress(
|
||||||
|
task.episode,
|
||||||
|
download.percentDownloaded.toInt()
|
||||||
|
)
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlinx.coroutines.delay(2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
||||||
|
logger("Exception while downloading file: ${e.message}")
|
||||||
|
snackString("Exception while downloading file: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
}
|
||||||
|
broadcastDownloadFailed(task.episode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
|
suspend fun hasDownloadStarted(
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
task: AnimeDownloadTask,
|
||||||
|
timeout: Long
|
||||||
|
): Boolean {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
while (System.currentTimeMillis() - startTime < timeout) {
|
||||||
|
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||||
|
if (download != null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Delay between each poll
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val directory = File(
|
||||||
|
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"${DownloadsManager.animeLocation}/${task.title}"
|
||||||
|
)
|
||||||
|
val episodeDirectory = File(directory, task.episode)
|
||||||
|
if (!directory.exists()) directory.mkdirs()
|
||||||
|
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
||||||
|
|
||||||
|
val file = File(directory, "media.json")
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
})
|
||||||
|
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
|
||||||
|
SAnimeImpl() // Provide an instance of SAnimeImpl
|
||||||
|
})
|
||||||
|
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
|
||||||
|
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
val mediaJson = gson.toJson(task.sourceMedia)
|
||||||
|
val media = gson.fromJson(mediaJson, Media::class.java)
|
||||||
|
if (media != null) {
|
||||||
|
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
|
||||||
|
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
|
||||||
|
if (task.episodeImage != null) {
|
||||||
|
media.anime?.episodes?.get(task.episode)?.let { episode ->
|
||||||
|
episode.thumb = downloadImage(
|
||||||
|
task.episodeImage,
|
||||||
|
episodeDirectory,
|
||||||
|
"episodeImage.jpg"
|
||||||
|
)?.let {
|
||||||
|
FileUrl(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonString = gson.toJson(media)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
file.writeText(jsonString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
println("Downloading url $url")
|
||||||
|
try {
|
||||||
|
connection = URL(url).openConnection() as HttpURLConnection
|
||||||
|
connection.connect()
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(directory, name)
|
||||||
|
FileOutputStream(file).use { output ->
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@withContext file.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
"Exception while saving ${name}: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadStarted(episodeNumber: String) {
|
||||||
|
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply {
|
||||||
|
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFinished(episodeNumber: String) {
|
||||||
|
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FINISHED).apply {
|
||||||
|
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFailed(episodeNumber: String) {
|
||||||
|
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FAILED).apply {
|
||||||
|
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadProgress(episodeNumber: String, progress: Int) {
|
||||||
|
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_PROGRESS).apply {
|
||||||
|
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||||
|
putExtra("progress", progress)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
|
||||||
|
val taskName = intent.getStringExtra(EXTRA_TASK_NAME)
|
||||||
|
taskName?.let {
|
||||||
|
cancelDownload(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class AnimeDownloadTask(
|
||||||
|
val title: String,
|
||||||
|
val episode: String,
|
||||||
|
val video: Video,
|
||||||
|
val subtitle: Subtitle? = null,
|
||||||
|
val sourceMedia: Media? = null,
|
||||||
|
val episodeImage: String? = null,
|
||||||
|
val retries: Int = 2,
|
||||||
|
val simultaneousDownloads: Int = 2,
|
||||||
|
) {
|
||||||
|
fun getTaskName(): String {
|
||||||
|
return "$title - $episode"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getTaskName(title: String, episode: String): String {
|
||||||
|
return "$title - $episode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_ID = 1103
|
||||||
|
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
|
||||||
|
const val EXTRA_TASK_NAME = "extra_task_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object AnimeServiceDataSingleton {
|
||||||
|
var video: Video? = null
|
||||||
|
var sourceMedia: Media? = null
|
||||||
|
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var isServiceRunning: Boolean = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package ani.dantotsu.download.anime
|
||||||
|
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.BaseAdapter
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import ani.dantotsu.R
|
||||||
|
|
||||||
|
|
||||||
|
class OfflineAnimeAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private var items: List<OfflineAnimeModel>,
|
||||||
|
private val searchListener: OfflineAnimeSearchListener
|
||||||
|
) : BaseAdapter() {
|
||||||
|
private val inflater: LayoutInflater =
|
||||||
|
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
private var originalItems: List<OfflineAnimeModel> = items
|
||||||
|
private var style =
|
||||||
|
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||||
|
|
||||||
|
override fun getCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): Any {
|
||||||
|
return items[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return position.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||||
|
|
||||||
|
val view: View = convertView ?: when (style) {
|
||||||
|
0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
|
||||||
|
1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
|
||||||
|
else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
|
||||||
|
}
|
||||||
|
|
||||||
|
val item = getItem(position) as OfflineAnimeModel
|
||||||
|
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||||
|
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||||
|
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||||
|
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||||
|
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||||
|
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||||
|
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||||
|
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
||||||
|
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
||||||
|
|
||||||
|
if (style == 0) {
|
||||||
|
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||||
|
val episodes = view.findViewById<TextView>(R.id.itemTotal)
|
||||||
|
episodes.text = " Episodes"
|
||||||
|
bannerView.setImageURI(item.banner)
|
||||||
|
totalepisodes.text = item.totalEpisodeList
|
||||||
|
} else if (style == 1) {
|
||||||
|
val watchedEpisodes =
|
||||||
|
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||||
|
watchedEpisodes.text = item.watchedEpisode
|
||||||
|
totalepisodes.text = " | " + item.totalEpisode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind item data to the views
|
||||||
|
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
|
||||||
|
type.text = item.type
|
||||||
|
typeView.visibility = View.VISIBLE
|
||||||
|
imageView.setImageURI(item.image)
|
||||||
|
titleTextView.text = item.title
|
||||||
|
itemScore.text = item.score
|
||||||
|
|
||||||
|
if (item.isOngoing) {
|
||||||
|
ongoing.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
ongoing.visibility = View.GONE
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchQuery(query: String) {
|
||||||
|
// Implement the filtering logic here, for example:
|
||||||
|
items = if (query.isEmpty()) {
|
||||||
|
// Return the original list if the query is empty
|
||||||
|
originalItems
|
||||||
|
} else {
|
||||||
|
// Filter the list based on the query
|
||||||
|
originalItems.filter { it.title.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
notifyDataSetChanged() // Notify the adapter that the data set has changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItems(items: List<OfflineAnimeModel>) {
|
||||||
|
this.items = items
|
||||||
|
this.originalItems = items
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyNewGrid() {
|
||||||
|
style =
|
||||||
|
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
package ani.dantotsu.download.anime
|
||||||
|
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AlphaAnimation
|
||||||
|
import android.view.animation.LayoutAnimationController
|
||||||
|
import android.view.animation.OvershootInterpolator
|
||||||
|
import android.widget.AbsListView
|
||||||
|
import android.widget.AutoCompleteTextView
|
||||||
|
import android.widget.GridView
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.util.Pair
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.marginBottom
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.bottomBar
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.loadData
|
||||||
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.InstanceCreator
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
|
|
||||||
|
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
private var downloads: List<OfflineAnimeModel> = listOf()
|
||||||
|
private lateinit var gridView: GridView
|
||||||
|
private lateinit var adapter: OfflineAnimeAdapter
|
||||||
|
private lateinit var total : TextView
|
||||||
|
private var uiSettings: UserInterfaceSettings =
|
||||||
|
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
|
||||||
|
|
||||||
|
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
|
||||||
|
textInputLayout.hint = "Anime"
|
||||||
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
|
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
|
||||||
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
|
val color = typedValue.data
|
||||||
|
|
||||||
|
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||||
|
animeUserAvatar.setSafeOnClickListener {
|
||||||
|
val dialogFragment =
|
||||||
|
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineANIME)
|
||||||
|
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||||
|
}
|
||||||
|
if (!uiSettings.immersiveMode) {
|
||||||
|
view.rootView.fitsSystemWindows = true
|
||||||
|
}
|
||||||
|
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getBoolean("colorOverflow", false) ?: false
|
||||||
|
if (!colorOverflow) {
|
||||||
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||||
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||||
|
searchView.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
onSearchQuery(s.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getInt("offline_view", 0)
|
||||||
|
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||||
|
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||||
|
var selected = when (style) {
|
||||||
|
0 -> layoutList
|
||||||
|
1 -> layoutcompact
|
||||||
|
else -> layoutList
|
||||||
|
}
|
||||||
|
selected.alpha = 1f
|
||||||
|
|
||||||
|
fun selected(it: ImageView) {
|
||||||
|
selected.alpha = 0.33f
|
||||||
|
selected = it
|
||||||
|
selected.alpha = 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutList.setOnClickListener {
|
||||||
|
selected(it as ImageView)
|
||||||
|
style = 0
|
||||||
|
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||||
|
?.putInt("offline_view", style!!)?.apply()
|
||||||
|
gridView.visibility = View.GONE
|
||||||
|
gridView = view.findViewById(R.id.gridView)
|
||||||
|
adapter.notifyNewGrid()
|
||||||
|
grid()
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutcompact.setOnClickListener {
|
||||||
|
selected(it as ImageView)
|
||||||
|
style = 1
|
||||||
|
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||||
|
?.putInt("offline_view", style!!)?.apply()
|
||||||
|
gridView.visibility = View.GONE
|
||||||
|
gridView = view.findViewById(R.id.gridView1)
|
||||||
|
adapter.notifyNewGrid()
|
||||||
|
grid()
|
||||||
|
}
|
||||||
|
|
||||||
|
gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
|
||||||
|
total = view.findViewById(R.id.total)
|
||||||
|
grid()
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun grid() {
|
||||||
|
gridView.visibility = View.VISIBLE
|
||||||
|
getDownloads()
|
||||||
|
val fadeIn = AlphaAnimation(0f, 1f)
|
||||||
|
fadeIn.duration = 300 // animations pog
|
||||||
|
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||||
|
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
||||||
|
gridView.adapter = adapter
|
||||||
|
gridView.scheduleLayoutAnimation()
|
||||||
|
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||||
|
gridView.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
// Get the OfflineAnimeModel that was clicked
|
||||||
|
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||||
|
val media =
|
||||||
|
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
|
media?.let {
|
||||||
|
val mediaModel = getMedia(it)
|
||||||
|
if (mediaModel == null) {
|
||||||
|
snackString("Error loading media.json")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||||
|
ContextCompat.startActivity(
|
||||||
|
requireActivity(),
|
||||||
|
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||||
|
.putExtra("download", true),
|
||||||
|
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
|
requireActivity(),
|
||||||
|
Pair.create(
|
||||||
|
requireActivity().findViewById<ImageView>(R.id.itemCompactImage),
|
||||||
|
ViewCompat.getTransitionName(requireActivity().findViewById(R.id.itemCompactImage))
|
||||||
|
),
|
||||||
|
).toBundle()
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
snackString("no media found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
|
// Get the OfflineAnimeModel that was clicked
|
||||||
|
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||||
|
val type: DownloadedType.Type =
|
||||||
|
DownloadedType.Type.ANIME
|
||||||
|
|
||||||
|
// Alert dialog to confirm deletion
|
||||||
|
val builder =
|
||||||
|
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||||
|
builder.setTitle("Delete ${item.title}?")
|
||||||
|
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||||
|
builder.setPositiveButton("Yes") { _, _ ->
|
||||||
|
downloadManager.removeMedia(item.title, type)
|
||||||
|
val mediaIds = requireContext().getSharedPreferences(
|
||||||
|
getString(R.string.anime_downloads),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
?.all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
|
||||||
|
if (mediaIds.isEmpty()) {
|
||||||
|
snackString("No media found") // if this happens, terrible things have happened
|
||||||
|
}
|
||||||
|
for (mediaId in mediaIds) {
|
||||||
|
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
|
||||||
|
.removeDownload(mediaId.toString())
|
||||||
|
}
|
||||||
|
getDownloads()
|
||||||
|
adapter.setItems(downloads)
|
||||||
|
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||||
|
}
|
||||||
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
val dialog = builder.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSearchQuery(query: String) {
|
||||||
|
adapter.onSearchQuery(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
var height = statusBarHeight
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||||
|
if (displayCutout != null) {
|
||||||
|
if (displayCutout.boundingRects.size > 0) {
|
||||||
|
height = max(
|
||||||
|
statusBarHeight,
|
||||||
|
min(
|
||||||
|
displayCutout.boundingRects[0].width(),
|
||||||
|
displayCutout.boundingRects[0].height()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||||
|
scrollTop.translationY =
|
||||||
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
|
val visible = false
|
||||||
|
|
||||||
|
fun animate() {
|
||||||
|
val start = if (visible) 0f else 1f
|
||||||
|
val end = if (!visible) 0f else 1f
|
||||||
|
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
|
||||||
|
duration = 300
|
||||||
|
interpolator = OvershootInterpolator(2f)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
|
||||||
|
duration = 300
|
||||||
|
interpolator = OvershootInterpolator(2f)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTop.setOnClickListener {
|
||||||
|
gridView.smoothScrollToPositionFromTop(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming 'scrollTop' is a view that you want to hide/show
|
||||||
|
scrollTop.visibility = View.GONE
|
||||||
|
|
||||||
|
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||||
|
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||||
|
// Implement behavior for different scroll states if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(
|
||||||
|
view: AbsListView,
|
||||||
|
firstVisibleItem: Int,
|
||||||
|
visibleItemCount: Int,
|
||||||
|
totalItemCount: Int
|
||||||
|
) {
|
||||||
|
val first = view.getChildAt(0)
|
||||||
|
val visibility = first != null && first.top < -height
|
||||||
|
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
initActivity(requireActivity())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
getDownloads()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
downloads = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
downloads = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
downloads = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDownloads() {
|
||||||
|
downloads = listOf()
|
||||||
|
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||||
|
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||||
|
for (title in animeTitles) {
|
||||||
|
val _downloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||||
|
val download = _downloads.first()
|
||||||
|
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||||
|
newAnimeDownloads += offlineAnimeModel
|
||||||
|
}
|
||||||
|
downloads = newAnimeDownloads
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
|
val type = if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
"Anime"
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
|
"Manga"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
)
|
||||||
|
//load media.json and convert to media class with gson
|
||||||
|
return try {
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
})
|
||||||
|
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
|
||||||
|
SAnimeImpl() // Provide an instance of SAnimeImpl
|
||||||
|
})
|
||||||
|
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
|
||||||
|
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
val media = File(directory, "media.json")
|
||||||
|
val mediaJson = media.readText()
|
||||||
|
gson.fromJson(mediaJson, Media::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Error loading media.json: ${e.message}")
|
||||||
|
logger(e.printStackTrace())
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||||
|
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
|
"Manga"
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
"Anime"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
)
|
||||||
|
//load media.json and convert to media class with gson
|
||||||
|
try {
|
||||||
|
val media = File(directory, "media.json")
|
||||||
|
val mediaJson = media.readText()
|
||||||
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
|
val cover = File(directory, "cover.jpg")
|
||||||
|
val coverUri: Uri? = if (cover.exists()) {
|
||||||
|
Uri.fromFile(cover)
|
||||||
|
} else null
|
||||||
|
val banner = File(directory, "banner.jpg")
|
||||||
|
val bannerUri: Uri? = if (banner.exists()) {
|
||||||
|
Uri.fromFile(banner)
|
||||||
|
} else null
|
||||||
|
val title = mediaModel.mainName()
|
||||||
|
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||||
|
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||||
|
val isOngoing =
|
||||||
|
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||||
|
val isUserScored = mediaModel.userScore != 0
|
||||||
|
val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
|
||||||
|
val totalEpisode =
|
||||||
|
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
|
||||||
|
?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
|
||||||
|
val chapters = " Chapters"
|
||||||
|
val totalEpisodesList =
|
||||||
|
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
|
||||||
|
?: "~").toString()
|
||||||
|
return OfflineAnimeModel(
|
||||||
|
title,
|
||||||
|
score,
|
||||||
|
totalEpisode,
|
||||||
|
totalEpisodesList,
|
||||||
|
watchedEpisodes,
|
||||||
|
type,
|
||||||
|
chapters,
|
||||||
|
isOngoing,
|
||||||
|
isUserScored,
|
||||||
|
coverUri,
|
||||||
|
bannerUri
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Error loading media.json: ${e.message}")
|
||||||
|
logger(e.printStackTrace())
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
return OfflineAnimeModel(
|
||||||
|
"unknown",
|
||||||
|
"0",
|
||||||
|
"??",
|
||||||
|
"??",
|
||||||
|
"??",
|
||||||
|
"movie",
|
||||||
|
"hmm",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OfflineAnimeSearchListener {
|
||||||
|
fun onSearchQuery(query: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ani.dantotsu.download.anime
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class OfflineAnimeModel(
|
||||||
|
val title: String,
|
||||||
|
val score: String,
|
||||||
|
val totalEpisode: String,
|
||||||
|
val totalEpisodeList: String,
|
||||||
|
val watchedEpisode: String,
|
||||||
|
val type: String,
|
||||||
|
val episodes: String,
|
||||||
|
val isOngoing: Boolean,
|
||||||
|
val isUserScored: Boolean,
|
||||||
|
val image: Uri?,
|
||||||
|
val banner: Uri?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
package ani.dantotsu.download.manga
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.manga.ImageData
|
||||||
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
||||||
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||||
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS
|
||||||
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||||
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.InstanceCreator
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.Queue
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
class MangaDownloaderService : Service() {
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
|
private lateinit var builder: NotificationCompat.Builder
|
||||||
|
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
|
private val downloadJobs = mutableMapOf<String, Job>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private var isCurrentlyProcessing = false
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
// This is only required for bound services.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
notificationManager = NotificationManagerCompat.from(this)
|
||||||
|
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||||
|
setContentTitle("Manga Download Progress")
|
||||||
|
setSmallIcon(R.drawable.ic_round_download_24)
|
||||||
|
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
builder.build(),
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
this,
|
||||||
|
cancelReceiver,
|
||||||
|
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
MangaServiceDataSingleton.downloadQueue.clear()
|
||||||
|
downloadJobs.clear()
|
||||||
|
MangaServiceDataSingleton.isServiceRunning = false
|
||||||
|
unregisterReceiver(cancelReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
snackString("Download started")
|
||||||
|
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
serviceScope.launch {
|
||||||
|
mutex.withLock {
|
||||||
|
if (!isCurrentlyProcessing) {
|
||||||
|
isCurrentlyProcessing = true
|
||||||
|
processQueue()
|
||||||
|
isCurrentlyProcessing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processQueue() {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (MangaServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||||
|
val task = MangaServiceDataSingleton.downloadQueue.poll()
|
||||||
|
if (task != null) {
|
||||||
|
val job = launch { download(task) }
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[task.chapter] = job
|
||||||
|
}
|
||||||
|
job.join() // Wait for the job to complete before continuing to the next task
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs.remove(task.chapter)
|
||||||
|
}
|
||||||
|
updateNotification() // Update the notification after each task is completed
|
||||||
|
}
|
||||||
|
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
stopSelf() // Stop the service when the queue is empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelDownload(chapter: String) {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[chapter]?.cancel()
|
||||||
|
downloadJobs.remove(chapter)
|
||||||
|
MangaServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
|
||||||
|
updateNotification() // Update the notification after cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification() {
|
||||||
|
// Update the notification to reflect the current state of the queue
|
||||||
|
val pendingDownloads = MangaServiceDataSingleton.downloadQueue.size
|
||||||
|
val text = if (pendingDownloads > 0) {
|
||||||
|
"Pending downloads: $pendingDownloads"
|
||||||
|
} else {
|
||||||
|
"All downloads completed"
|
||||||
|
}
|
||||||
|
builder.setContentText(text)
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun download(task: DownloadTask) {
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
this@MangaDownloaderService,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||||
|
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||||
|
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through each ImageData object from the task
|
||||||
|
var farthest = 0
|
||||||
|
for ((index, image) in task.imageData.withIndex()) {
|
||||||
|
if (deferredMap.size >= task.simultaneousDownloads) {
|
||||||
|
deferredMap.values.awaitAll()
|
||||||
|
deferredMap.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
deferredMap[index] = async(Dispatchers.IO) {
|
||||||
|
var bitmap: Bitmap? = null
|
||||||
|
var retryCount = 0
|
||||||
|
|
||||||
|
while (bitmap == null && retryCount < task.retries) {
|
||||||
|
bitmap = image.fetchAndProcessImage(
|
||||||
|
image.page,
|
||||||
|
image.source,
|
||||||
|
this@MangaDownloaderService
|
||||||
|
)
|
||||||
|
retryCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmap != null) {
|
||||||
|
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
||||||
|
}
|
||||||
|
farthest++
|
||||||
|
builder.setProgress(task.imageData.size, farthest, false)
|
||||||
|
broadcastDownloadProgress(
|
||||||
|
task.chapter,
|
||||||
|
farthest * 100 / task.imageData.size
|
||||||
|
)
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for any remaining deferred to complete
|
||||||
|
deferredMap.values.awaitAll()
|
||||||
|
|
||||||
|
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
|
||||||
|
saveMediaInfo(task)
|
||||||
|
downloadsManager.addDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.chapter,
|
||||||
|
DownloadedType.Type.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
broadcastDownloadFinished(task.chapter)
|
||||||
|
snackString("${task.title} - ${task.chapter} Download finished")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Exception while downloading file: ${e.message}")
|
||||||
|
snackString("Exception while downloading file: ${e.message}")
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
broadcastDownloadFailed(task.chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||||
|
try {
|
||||||
|
// Define the directory within the private external storage space
|
||||||
|
val directory = File(
|
||||||
|
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/$title/$chapter"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a file reference within that directory for your image
|
||||||
|
val file = File(directory, fileName)
|
||||||
|
|
||||||
|
// Use a FileOutputStream to write the bitmap to the file
|
||||||
|
FileOutputStream(file).use { outputStream ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Exception while saving image: ${e.message}")
|
||||||
|
snackString("Exception while saving image: ${e.message}")
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val directory = File(
|
||||||
|
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/${task.title}"
|
||||||
|
)
|
||||||
|
if (!directory.exists()) directory.mkdirs()
|
||||||
|
|
||||||
|
val file = File(directory, "media.json")
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
val mediaJson = gson.toJson(task.sourceMedia)
|
||||||
|
val media = gson.fromJson(mediaJson, Media::class.java)
|
||||||
|
if (media != null) {
|
||||||
|
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
|
||||||
|
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
|
||||||
|
|
||||||
|
val jsonString = gson.toJson(media)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
file.writeText(jsonString)
|
||||||
|
} catch (e: android.system.ErrnoException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Toast.makeText(
|
||||||
|
this@MangaDownloaderService,
|
||||||
|
"Error while saving: ${e.localizedMessage}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
println("Downloading url $url")
|
||||||
|
try {
|
||||||
|
connection = URL(url).openConnection() as HttpURLConnection
|
||||||
|
connection.connect()
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(directory, name)
|
||||||
|
FileOutputStream(file).use { output ->
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@withContext file.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MangaDownloaderService,
|
||||||
|
"Exception while saving ${name}: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadStarted(chapterNumber: String) {
|
||||||
|
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
|
||||||
|
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFinished(chapterNumber: String) {
|
||||||
|
val intent = Intent(ACTION_DOWNLOAD_FINISHED).apply {
|
||||||
|
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFailed(chapterNumber: String) {
|
||||||
|
val intent = Intent(ACTION_DOWNLOAD_FAILED).apply {
|
||||||
|
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadProgress(chapterNumber: String, progress: Int) {
|
||||||
|
val intent = Intent(ACTION_DOWNLOAD_PROGRESS).apply {
|
||||||
|
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||||
|
putExtra("progress", progress)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
|
||||||
|
val chapter = intent.getStringExtra(EXTRA_CHAPTER)
|
||||||
|
chapter?.let {
|
||||||
|
cancelDownload(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class DownloadTask(
|
||||||
|
val title: String,
|
||||||
|
val chapter: String,
|
||||||
|
val imageData: List<ImageData>,
|
||||||
|
val sourceMedia: Media? = null,
|
||||||
|
val retries: Int = 2,
|
||||||
|
val simultaneousDownloads: Int = 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_ID = 1103
|
||||||
|
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
|
||||||
|
const val EXTRA_CHAPTER = "extra_chapter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object MangaServiceDataSingleton {
|
||||||
|
var imageData: List<ImageData> = listOf()
|
||||||
|
var sourceMedia: Media? = null
|
||||||
|
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var isServiceRunning: Boolean = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package ani.dantotsu.download.manga
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.BaseAdapter
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import ani.dantotsu.R
|
||||||
|
|
||||||
|
|
||||||
|
class OfflineMangaAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private var items: List<OfflineMangaModel>,
|
||||||
|
private val searchListener: OfflineMangaSearchListener
|
||||||
|
) : BaseAdapter() {
|
||||||
|
private val inflater: LayoutInflater =
|
||||||
|
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
private var originalItems: List<OfflineMangaModel> = items
|
||||||
|
private var style =
|
||||||
|
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||||
|
|
||||||
|
override fun getCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): Any {
|
||||||
|
return items[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return position.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||||
|
|
||||||
|
val view: View = convertView ?: when (style) {
|
||||||
|
0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
|
||||||
|
1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
|
||||||
|
else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
|
||||||
|
}
|
||||||
|
|
||||||
|
val item = getItem(position) as OfflineMangaModel
|
||||||
|
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||||
|
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||||
|
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||||
|
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||||
|
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||||
|
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||||
|
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||||
|
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
||||||
|
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
||||||
|
|
||||||
|
if (style == 0) {
|
||||||
|
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||||
|
val chapters = view.findViewById<TextView>(R.id.itemTotal)
|
||||||
|
chapters.text = " Chapters"
|
||||||
|
bannerView.setImageURI(item.banner)
|
||||||
|
totalChapter.text = item.totalChapter
|
||||||
|
} else if (style == 1) {
|
||||||
|
val readChapter =
|
||||||
|
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||||
|
readChapter.text = item.readChapter
|
||||||
|
totalChapter.text = " | " + item.totalChapter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind item data to the views
|
||||||
|
typeImage.setImageResource(if (item.type == "Novel") R.drawable.ic_round_book_24 else R.drawable.ic_round_import_contacts_24)
|
||||||
|
type.text = item.type
|
||||||
|
typeView.visibility = View.VISIBLE
|
||||||
|
imageView.setImageURI(item.image)
|
||||||
|
titleTextView.text = item.title
|
||||||
|
itemScore.text = item.score
|
||||||
|
|
||||||
|
if (item.isOngoing) {
|
||||||
|
ongoing.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
ongoing.visibility = View.GONE
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchQuery(query: String) {
|
||||||
|
// Implement the filtering logic here, for example:
|
||||||
|
items = if (query.isEmpty()) {
|
||||||
|
// Return the original list if the query is empty
|
||||||
|
originalItems
|
||||||
|
} else {
|
||||||
|
// Filter the list based on the query
|
||||||
|
originalItems.filter { it.title.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
notifyDataSetChanged() // Notify the adapter that the data set has changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItems(items: List<OfflineMangaModel>) {
|
||||||
|
this.items = items
|
||||||
|
this.originalItems = items
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyNewGrid() {
|
||||||
|
style =
|
||||||
|
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
package ani.dantotsu.download.manga
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AlphaAnimation
|
||||||
|
import android.view.animation.LayoutAnimationController
|
||||||
|
import android.view.animation.OvershootInterpolator
|
||||||
|
import android.widget.AbsListView
|
||||||
|
import android.widget.AutoCompleteTextView
|
||||||
|
import android.widget.GridView
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.util.Pair
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.marginBottom
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.bottomBar
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.loadData
|
||||||
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.InstanceCreator
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
|
|
||||||
|
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
private var downloads: List<OfflineMangaModel> = listOf()
|
||||||
|
private lateinit var gridView: GridView
|
||||||
|
private lateinit var adapter: OfflineMangaAdapter
|
||||||
|
private lateinit var total: TextView
|
||||||
|
private var uiSettings: UserInterfaceSettings =
|
||||||
|
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
|
||||||
|
|
||||||
|
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
|
||||||
|
textInputLayout.hint = "Manga"
|
||||||
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
|
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
|
||||||
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
|
val color = typedValue.data
|
||||||
|
|
||||||
|
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||||
|
animeUserAvatar.setSafeOnClickListener {
|
||||||
|
val dialogFragment =
|
||||||
|
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
|
||||||
|
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||||
|
}
|
||||||
|
if (!uiSettings.immersiveMode) {
|
||||||
|
view.rootView.fitsSystemWindows = true
|
||||||
|
}
|
||||||
|
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getBoolean("colorOverflow", false) ?: false
|
||||||
|
if (!colorOverflow) {
|
||||||
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||||
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||||
|
searchView.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
onSearchQuery(s.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getInt("offline_view", 0)
|
||||||
|
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||||
|
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||||
|
var selected = when (style) {
|
||||||
|
0 -> layoutList
|
||||||
|
1 -> layoutcompact
|
||||||
|
else -> layoutList
|
||||||
|
}
|
||||||
|
selected.alpha = 1f
|
||||||
|
|
||||||
|
fun selected(it: ImageView) {
|
||||||
|
selected.alpha = 0.33f
|
||||||
|
selected = it
|
||||||
|
selected.alpha = 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutList.setOnClickListener {
|
||||||
|
selected(it as ImageView)
|
||||||
|
style = 0
|
||||||
|
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||||
|
.putInt("offline_view", style!!).apply()
|
||||||
|
gridView.visibility = View.GONE
|
||||||
|
gridView = view.findViewById(R.id.gridView)
|
||||||
|
adapter.notifyNewGrid()
|
||||||
|
grid()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutcompact.setOnClickListener {
|
||||||
|
selected(it as ImageView)
|
||||||
|
style = 1
|
||||||
|
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||||
|
.putInt("offline_view", style!!).apply()
|
||||||
|
gridView.visibility = View.GONE
|
||||||
|
gridView = view.findViewById(R.id.gridView1)
|
||||||
|
adapter.notifyNewGrid()
|
||||||
|
grid()
|
||||||
|
}
|
||||||
|
gridView =
|
||||||
|
if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
|
||||||
|
total = view.findViewById(R.id.total)
|
||||||
|
grid()
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun grid() {
|
||||||
|
gridView.visibility = View.VISIBLE
|
||||||
|
getDownloads()
|
||||||
|
val fadeIn = AlphaAnimation(0f, 1f)
|
||||||
|
fadeIn.duration = 300 // animations pog
|
||||||
|
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||||
|
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||||
|
gridView.adapter = adapter
|
||||||
|
gridView.scheduleLayoutAnimation()
|
||||||
|
total.text =
|
||||||
|
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||||
|
gridView.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
// Get the OfflineMangaModel that was clicked
|
||||||
|
val item = adapter.getItem(position) as OfflineMangaModel
|
||||||
|
val media =
|
||||||
|
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
|
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
|
media?.let {
|
||||||
|
ContextCompat.startActivity(
|
||||||
|
requireActivity(),
|
||||||
|
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||||
|
.putExtra("media", getMedia(it))
|
||||||
|
.putExtra("download", true),
|
||||||
|
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
|
requireActivity(),
|
||||||
|
Pair.create(
|
||||||
|
gridView.getChildAt(position)
|
||||||
|
.findViewById<ImageView>(R.id.itemCompactImage),
|
||||||
|
ViewCompat.getTransitionName(requireActivity().findViewById(R.id.itemCompactImage))
|
||||||
|
)
|
||||||
|
).toBundle()
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
snackString("no media found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
|
// Get the OfflineMangaModel that was clicked
|
||||||
|
val item = adapter.getItem(position) as OfflineMangaModel
|
||||||
|
val type: DownloadedType.Type =
|
||||||
|
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||||
|
DownloadedType.Type.MANGA
|
||||||
|
} else {
|
||||||
|
DownloadedType.Type.NOVEL
|
||||||
|
}
|
||||||
|
// Alert dialog to confirm deletion
|
||||||
|
val builder =
|
||||||
|
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||||
|
builder.setTitle("Delete ${item.title}?")
|
||||||
|
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||||
|
builder.setPositiveButton("Yes") { _, _ ->
|
||||||
|
downloadManager.removeMedia(item.title, type)
|
||||||
|
getDownloads()
|
||||||
|
adapter.setItems(downloads)
|
||||||
|
total.text =
|
||||||
|
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||||
|
}
|
||||||
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
val dialog = builder.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSearchQuery(query: String) {
|
||||||
|
adapter.onSearchQuery(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initActivity(requireActivity())
|
||||||
|
var height = statusBarHeight
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||||
|
if (displayCutout != null) {
|
||||||
|
if (displayCutout.boundingRects.size > 0) {
|
||||||
|
height = max(
|
||||||
|
statusBarHeight,
|
||||||
|
min(
|
||||||
|
displayCutout.boundingRects[0].width(),
|
||||||
|
displayCutout.boundingRects[0].height()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||||
|
scrollTop.translationY =
|
||||||
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
|
val visible = false
|
||||||
|
|
||||||
|
fun animate() {
|
||||||
|
val start = if (visible) 0f else 1f
|
||||||
|
val end = if (!visible) 0f else 1f
|
||||||
|
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
|
||||||
|
duration = 300
|
||||||
|
interpolator = OvershootInterpolator(2f)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
|
||||||
|
duration = 300
|
||||||
|
interpolator = OvershootInterpolator(2f)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTop.setOnClickListener {
|
||||||
|
gridView.smoothScrollToPositionFromTop(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming 'scrollTop' is a view that you want to hide/show
|
||||||
|
scrollTop.visibility = View.GONE
|
||||||
|
|
||||||
|
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||||
|
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||||
|
// Implement behavior for different scroll states if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(
|
||||||
|
view: AbsListView,
|
||||||
|
firstVisibleItem: Int,
|
||||||
|
visibleItemCount: Int,
|
||||||
|
totalItemCount: Int
|
||||||
|
) {
|
||||||
|
val first = view.getChildAt(0)
|
||||||
|
val visibility = first != null && first.top < -height
|
||||||
|
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
getDownloads()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
downloads = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
downloads = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
downloads = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDownloads() {
|
||||||
|
downloads = listOf()
|
||||||
|
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||||
|
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
|
for (title in mangaTitles) {
|
||||||
|
val _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||||
|
val download = _downloads.first()
|
||||||
|
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||||
|
newMangaDownloads += offlineMangaModel
|
||||||
|
}
|
||||||
|
downloads = newMangaDownloads
|
||||||
|
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||||
|
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
|
for (title in novelTitles) {
|
||||||
|
val _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||||
|
val download = _downloads.first()
|
||||||
|
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||||
|
newNovelDownloads += offlineMangaModel
|
||||||
|
}
|
||||||
|
downloads += newNovelDownloads
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
|
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
|
"Manga"
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
"Anime"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
)
|
||||||
|
//load media.json and convert to media class with gson
|
||||||
|
return try {
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
val media = File(directory, "media.json")
|
||||||
|
val mediaJson = media.readText()
|
||||||
|
gson.fromJson(mediaJson, Media::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Error loading media.json: ${e.message}")
|
||||||
|
logger(e.printStackTrace())
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||||
|
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
|
"Manga"
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
"Anime"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
)
|
||||||
|
//load media.json and convert to media class with gson
|
||||||
|
try {
|
||||||
|
val media = File(directory, "media.json")
|
||||||
|
val mediaJson = media.readText()
|
||||||
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
|
val cover = File(directory, "cover.jpg")
|
||||||
|
val coverUri: Uri? = if (cover.exists()) {
|
||||||
|
Uri.fromFile(cover)
|
||||||
|
} else null
|
||||||
|
val banner = File(directory, "banner.jpg")
|
||||||
|
val bannerUri: Uri? = if (banner.exists()) {
|
||||||
|
Uri.fromFile(banner)
|
||||||
|
} else null
|
||||||
|
val title = mediaModel.mainName()
|
||||||
|
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||||
|
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||||
|
val isOngoing =
|
||||||
|
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||||
|
val isUserScored = mediaModel.userScore != 0
|
||||||
|
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||||
|
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||||
|
val chapters = " Chapters"
|
||||||
|
return OfflineMangaModel(
|
||||||
|
title,
|
||||||
|
score,
|
||||||
|
totalchapter,
|
||||||
|
readchapter,
|
||||||
|
type,
|
||||||
|
chapters,
|
||||||
|
isOngoing,
|
||||||
|
isUserScored,
|
||||||
|
coverUri,
|
||||||
|
bannerUri
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Error loading media.json: ${e.message}")
|
||||||
|
logger(e.printStackTrace())
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
return OfflineMangaModel(
|
||||||
|
"unknown",
|
||||||
|
"0",
|
||||||
|
"??",
|
||||||
|
"??",
|
||||||
|
"movie",
|
||||||
|
"hmm",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OfflineMangaSearchListener {
|
||||||
|
fun onSearchQuery(query: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ani.dantotsu.download.manga
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class OfflineMangaModel(
|
||||||
|
val title: String,
|
||||||
|
val score: String,
|
||||||
|
val totalChapter: String,
|
||||||
|
val readChapter: String,
|
||||||
|
val type: String,
|
||||||
|
val chapters: String,
|
||||||
|
val isOngoing: Boolean,
|
||||||
|
val isUserScored: Boolean,
|
||||||
|
val image: Uri?,
|
||||||
|
val banner: Uri?
|
||||||
|
)
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
package ani.dantotsu.download.novel
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.novel.NovelReadFragment
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.InstanceCreator
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Request
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.Queue
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
class NovelDownloaderService : Service() {
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
|
private lateinit var builder: NotificationCompat.Builder
|
||||||
|
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
|
private val downloadJobs = mutableMapOf<String, Job>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private var isCurrentlyProcessing = false
|
||||||
|
|
||||||
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
// This is only required for bound services.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
notificationManager = NotificationManagerCompat.from(this)
|
||||||
|
builder =
|
||||||
|
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||||
|
setContentTitle("Novel Download Progress")
|
||||||
|
setSmallIcon(R.drawable.ic_round_download_24)
|
||||||
|
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
builder.build(),
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
this,
|
||||||
|
cancelReceiver,
|
||||||
|
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
NovelServiceDataSingleton.downloadQueue.clear()
|
||||||
|
downloadJobs.clear()
|
||||||
|
NovelServiceDataSingleton.isServiceRunning = false
|
||||||
|
unregisterReceiver(cancelReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
snackString("Download started")
|
||||||
|
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
serviceScope.launch {
|
||||||
|
mutex.withLock {
|
||||||
|
if (!isCurrentlyProcessing) {
|
||||||
|
isCurrentlyProcessing = true
|
||||||
|
processQueue()
|
||||||
|
isCurrentlyProcessing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processQueue() {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (NovelServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||||
|
val task = NovelServiceDataSingleton.downloadQueue.poll()
|
||||||
|
if (task != null) {
|
||||||
|
val job = launch { download(task) }
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[task.chapter] = job
|
||||||
|
}
|
||||||
|
job.join() // Wait for the job to complete before continuing to the next task
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs.remove(task.chapter)
|
||||||
|
}
|
||||||
|
updateNotification() // Update the notification after each task is completed
|
||||||
|
}
|
||||||
|
if (NovelServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
stopSelf() // Stop the service when the queue is empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelDownload(chapter: String) {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[chapter]?.cancel()
|
||||||
|
downloadJobs.remove(chapter)
|
||||||
|
NovelServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
|
||||||
|
updateNotification() // Update the notification after cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification() {
|
||||||
|
// Update the notification to reflect the current state of the queue
|
||||||
|
val pendingDownloads = NovelServiceDataSingleton.downloadQueue.size
|
||||||
|
val text = if (pendingDownloads > 0) {
|
||||||
|
"Pending downloads: $pendingDownloads"
|
||||||
|
} else {
|
||||||
|
"All downloads completed"
|
||||||
|
}
|
||||||
|
builder.setContentText(text)
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun isEpubFile(urlString: String): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(urlString)
|
||||||
|
.head()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
networkHelper.client.newCall(request).execute().use { response ->
|
||||||
|
val contentType = response.header("Content-Type")
|
||||||
|
val contentDisposition = response.header("Content-Disposition")
|
||||||
|
|
||||||
|
logger("Content-Type: $contentType")
|
||||||
|
logger("Content-Disposition: $contentDisposition")
|
||||||
|
|
||||||
|
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
|
||||||
|
contentType == "application/epub+zip" ||
|
||||||
|
(contentDisposition?.contains(".epub") == true)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Error checking file type: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAlreadyDownloaded(urlString: String): Boolean {
|
||||||
|
return urlString.contains("file://")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun download(task: DownloadTask) {
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
this@NovelDownloaderService,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastDownloadStarted(task.originalLink)
|
||||||
|
|
||||||
|
if (notifi) {
|
||||||
|
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEpubFile(task.downloadLink)) {
|
||||||
|
if (isAlreadyDownloaded(task.originalLink)) {
|
||||||
|
logger("Already downloaded")
|
||||||
|
broadcastDownloadFinished(task.originalLink)
|
||||||
|
snackString("Already downloaded")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
logger("Download link is not an .epub file")
|
||||||
|
broadcastDownloadFailed(task.originalLink)
|
||||||
|
snackString("Download link is not an .epub file")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the download
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(task.downloadLink)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
networkHelper.downloadClient.newCall(request).execute().use { response ->
|
||||||
|
// Ensure the response is successful and has a body
|
||||||
|
if (!response.isSuccessful || response.body == null) {
|
||||||
|
throw IOException("Failed to download file: ${response.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(
|
||||||
|
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create directories if they don't exist
|
||||||
|
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
|
||||||
|
|
||||||
|
// Overwrite existing file
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
|
||||||
|
//download cover
|
||||||
|
task.coverUrl?.let {
|
||||||
|
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
||||||
|
}
|
||||||
|
|
||||||
|
val sink = file.sink().buffer()
|
||||||
|
val responseBody = response.body
|
||||||
|
val totalBytes = responseBody.contentLength()
|
||||||
|
var downloadedBytes = 0L
|
||||||
|
|
||||||
|
val notificationUpdateInterval = 1024 * 1024 // 1 MB
|
||||||
|
val broadcastUpdateInterval = 1024 * 256 // 256 KB
|
||||||
|
var lastNotificationUpdate = 0L
|
||||||
|
var lastBroadcastUpdate = 0L
|
||||||
|
|
||||||
|
responseBody.source().use { source ->
|
||||||
|
while (true) {
|
||||||
|
val read = source.read(sink.buffer, 8192)
|
||||||
|
if (read == -1L) break
|
||||||
|
downloadedBytes += read
|
||||||
|
sink.emit()
|
||||||
|
|
||||||
|
// Update progress at intervals
|
||||||
|
if (downloadedBytes - lastNotificationUpdate >= notificationUpdateInterval) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val progress =
|
||||||
|
(downloadedBytes * 100 / totalBytes).toInt()
|
||||||
|
builder.setProgress(100, progress, false)
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
builder.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastNotificationUpdate = downloadedBytes
|
||||||
|
}
|
||||||
|
if (downloadedBytes - lastBroadcastUpdate >= broadcastUpdateInterval) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val progress =
|
||||||
|
(downloadedBytes * 100 / totalBytes).toInt()
|
||||||
|
logger("Download progress: $progress")
|
||||||
|
broadcastDownloadProgress(task.originalLink, progress)
|
||||||
|
}
|
||||||
|
lastBroadcastUpdate = downloadedBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sink.close()
|
||||||
|
//if the file is smaller than 95% of totalBytes, it means the download was interrupted
|
||||||
|
if (file.length() < totalBytes * 0.95) {
|
||||||
|
throw IOException("Failed to download file: ${response.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Exception while downloading .epub inside request: ${e.message}")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update notification for download completion
|
||||||
|
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMediaInfo(task)
|
||||||
|
downloadsManager.addDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.chapter,
|
||||||
|
DownloadedType.Type.NOVEL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
broadcastDownloadFinished(task.originalLink)
|
||||||
|
snackString("${task.title} - ${task.chapter} Download finished")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Exception while downloading .epub: ${e.message}")
|
||||||
|
snackString("Exception while downloading .epub: ${e.message}")
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
broadcastDownloadFailed(task.originalLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val directory = File(
|
||||||
|
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${task.title}"
|
||||||
|
)
|
||||||
|
if (!directory.exists()) directory.mkdirs()
|
||||||
|
|
||||||
|
val file = File(directory, "media.json")
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
val mediaJson = gson.toJson(task.sourceMedia)
|
||||||
|
val media = gson.fromJson(mediaJson, Media::class.java)
|
||||||
|
if (media != null) {
|
||||||
|
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
|
||||||
|
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
|
||||||
|
|
||||||
|
val jsonString = gson.toJson(media)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
file.writeText(jsonString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||||
|
withContext(
|
||||||
|
Dispatchers.IO
|
||||||
|
) {
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
println("Downloading url $url")
|
||||||
|
try {
|
||||||
|
connection = URL(url).openConnection() as HttpURLConnection
|
||||||
|
connection.connect()
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(directory, name)
|
||||||
|
FileOutputStream(file).use { output ->
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@withContext file.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@NovelDownloaderService,
|
||||||
|
"Exception while saving ${name}: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadStarted(link: String) {
|
||||||
|
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_STARTED).apply {
|
||||||
|
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFinished(link: String) {
|
||||||
|
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FINISHED).apply {
|
||||||
|
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFailed(link: String) {
|
||||||
|
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FAILED).apply {
|
||||||
|
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadProgress(link: String, progress: Int) {
|
||||||
|
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_PROGRESS).apply {
|
||||||
|
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||||
|
putExtra("progress", progress)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
|
||||||
|
val chapter = intent.getStringExtra(EXTRA_CHAPTER)
|
||||||
|
chapter?.let {
|
||||||
|
cancelDownload(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class DownloadTask(
|
||||||
|
val title: String,
|
||||||
|
val chapter: String,
|
||||||
|
val downloadLink: String,
|
||||||
|
val originalLink: String,
|
||||||
|
val sourceMedia: Media? = null,
|
||||||
|
val coverUrl: String? = null,
|
||||||
|
val retries: Int = 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_ID = 1103
|
||||||
|
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
|
||||||
|
const val EXTRA_CHAPTER = "extra_chapter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object NovelServiceDataSingleton {
|
||||||
|
var sourceMedia: Media? = null
|
||||||
|
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var isServiceRunning: Boolean = false
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) {
|
class ExoplayerDownloadService :
|
||||||
|
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val JOB_ID = 1
|
private const val JOB_ID = 1
|
||||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||||
@@ -21,10 +22,13 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
|
|||||||
|
|
||||||
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
||||||
|
|
||||||
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
|
override fun getForegroundNotification(
|
||||||
|
downloads: MutableList<Download>,
|
||||||
|
notMetRequirements: Int
|
||||||
|
): Notification =
|
||||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||||
this,
|
this,
|
||||||
R.drawable.monochrome,
|
R.drawable.mono,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
downloads,
|
downloads,
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
package ani.dantotsu.download.video
|
package ani.dantotsu.download.video
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.ContextCompat.getString
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
@@ -15,6 +26,7 @@ import androidx.media3.datasource.cache.NoOpCacheEvictor
|
|||||||
import androidx.media3.datasource.cache.SimpleCache
|
import androidx.media3.datasource.cache.SimpleCache
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
|
import androidx.media3.exoplayer.offline.Download
|
||||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||||
import androidx.media3.exoplayer.offline.DownloadManager
|
import androidx.media3.exoplayer.offline.DownloadManager
|
||||||
import androidx.media3.exoplayer.offline.DownloadService
|
import androidx.media3.exoplayer.offline.DownloadService
|
||||||
@@ -22,22 +34,33 @@ import androidx.media3.exoplayer.scheduler.Requirements
|
|||||||
import androidx.media3.ui.TrackSelectionDialogBuilder
|
import androidx.media3.ui.TrackSelectionDialogBuilder
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.defaultHeaders
|
import ani.dantotsu.defaultHeaders
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
|
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.okHttpClient
|
import ani.dantotsu.okHttpClient
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
import ani.dantotsu.parsers.SubtitleType
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
import ani.dantotsu.parsers.Video
|
import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.parsers.VideoType
|
import ani.dantotsu.parsers.VideoType
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.*
|
||||||
|
|
||||||
object Helper {
|
object Helper {
|
||||||
|
|
||||||
|
private var simpleCache: SimpleCache? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||||
val dataSourceFactory = DataSource.Factory {
|
val dataSourceFactory = DataSource.Factory {
|
||||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
val dataSource: HttpDataSource =
|
||||||
|
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
defaultHeaders.forEach {
|
defaultHeaders.forEach {
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
dataSource.setRequestProperty(it.key, it.value)
|
||||||
}
|
}
|
||||||
@@ -63,6 +86,7 @@ object Helper {
|
|||||||
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
||||||
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
||||||
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
||||||
|
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
@@ -77,26 +101,13 @@ object Helper {
|
|||||||
)
|
)
|
||||||
downloadHelper.prepare(object : DownloadHelper.Callback {
|
downloadHelper.prepare(object : DownloadHelper.Callback {
|
||||||
override fun onPrepared(helper: DownloadHelper) {
|
override fun onPrepared(helper: DownloadHelper) {
|
||||||
TrackSelectionDialogBuilder(context,"Select thingy",helper.getTracks(0).groups
|
helper.getDownloadRequest(null).let {
|
||||||
) { _, overrides ->
|
|
||||||
val params = TrackSelectionParameters.Builder(context)
|
|
||||||
overrides.forEach{
|
|
||||||
params.addOverride(it.value)
|
|
||||||
}
|
|
||||||
helper.addTrackSelection(0, params.build())
|
|
||||||
MyDownloadService
|
|
||||||
DownloadService.sendAddDownload(
|
DownloadService.sendAddDownload(
|
||||||
context,
|
context,
|
||||||
MyDownloadService::class.java,
|
ExoplayerDownloadService::class.java,
|
||||||
helper.getDownloadRequest(null),
|
it,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}.apply {
|
|
||||||
setTheme(R.style.DialogTheme)
|
|
||||||
setTrackNameProvider {
|
|
||||||
if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
|
|
||||||
}
|
|
||||||
build().show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,31 +119,61 @@ object Helper {
|
|||||||
|
|
||||||
|
|
||||||
private var download: DownloadManager? = null
|
private var download: DownloadManager? = null
|
||||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads"
|
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun downloadManager(context: Context): DownloadManager {
|
fun downloadManager(context: Context): DownloadManager {
|
||||||
return download ?: let {
|
return download ?: let {
|
||||||
val database = StandaloneDatabaseProvider(context)
|
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||||
val dataSourceFactory = DataSource.Factory {
|
val dataSourceFactory = DataSource.Factory {
|
||||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
val okHttpClient = networkHelper.client
|
||||||
|
val dataSource: HttpDataSource =
|
||||||
|
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
defaultHeaders.forEach {
|
defaultHeaders.forEach {
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
dataSource.setRequestProperty(it.key, it.value)
|
||||||
}
|
}
|
||||||
dataSource
|
dataSource
|
||||||
}
|
}
|
||||||
DownloadManager(
|
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||||
|
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||||
|
val downloadManager = DownloadManager(
|
||||||
context,
|
context,
|
||||||
database,
|
database,
|
||||||
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database),
|
getSimpleCache(context),
|
||||||
dataSourceFactory,
|
dataSourceFactory,
|
||||||
Executor(Runnable::run)
|
executorService
|
||||||
).apply {
|
).apply {
|
||||||
requirements = Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
requirements =
|
||||||
|
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||||
maxParallelDownloads = 3
|
maxParallelDownloads = 3
|
||||||
}
|
}
|
||||||
|
downloadManager.addListener( //for testing
|
||||||
|
object : DownloadManager.Listener {
|
||||||
|
override fun onDownloadChanged(
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
download: Download,
|
||||||
|
finalException: Exception?
|
||||||
|
) {
|
||||||
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
|
Log.e("Downloader", "Download Completed")
|
||||||
|
} else if (download.state == Download.STATE_FAILED) {
|
||||||
|
Log.e("Downloader", "Download Failed")
|
||||||
|
} else if (download.state == Download.STATE_STOPPED) {
|
||||||
|
Log.e("Downloader", "Download Stopped")
|
||||||
|
} else if (download.state == Download.STATE_QUEUED) {
|
||||||
|
Log.e("Downloader", "Download Queued")
|
||||||
|
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||||
|
Log.e("Downloader", "Download Downloading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
downloadManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,4 +189,108 @@ object Helper {
|
|||||||
}
|
}
|
||||||
return downloadDirectory!!
|
return downloadDirectory!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun startAnimeDownloadService(
|
||||||
|
context: Context,
|
||||||
|
title: String,
|
||||||
|
episode: String,
|
||||||
|
video: Video,
|
||||||
|
subtitle: Subtitle? = null,
|
||||||
|
sourceMedia: Media? = null,
|
||||||
|
episodeImage: String? = null
|
||||||
|
) {
|
||||||
|
if (!isNotificationPermissionGranted(context)) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
context as Activity,
|
||||||
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
|
||||||
|
title,
|
||||||
|
episode,
|
||||||
|
video,
|
||||||
|
subtitle,
|
||||||
|
sourceMedia,
|
||||||
|
episodeImage
|
||||||
|
)
|
||||||
|
|
||||||
|
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||||
|
val downloadCheck = downloadsManger
|
||||||
|
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
||||||
|
|
||||||
|
if (downloadCheck) {
|
||||||
|
AlertDialog.Builder(context, R.style.MyPopup)
|
||||||
|
.setTitle("Download Exists")
|
||||||
|
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||||
|
.setPositiveButton("Yes") { _, _ ->
|
||||||
|
DownloadService.sendRemoveDownload(
|
||||||
|
context,
|
||||||
|
ExoplayerDownloadService::class.java,
|
||||||
|
context.getSharedPreferences(
|
||||||
|
getString(context, R.string.anime_downloads),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).getString(
|
||||||
|
animeDownloadTask.getTaskName(),
|
||||||
|
""
|
||||||
|
) ?: "",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
context.getSharedPreferences(
|
||||||
|
getString(context, R.string.anime_downloads),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).edit()
|
||||||
|
.remove(animeDownloadTask.getTaskName())
|
||||||
|
.apply()
|
||||||
|
downloadsManger.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
title,
|
||||||
|
episode,
|
||||||
|
DownloadedType.Type.ANIME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||||
|
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||||
|
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
AnimeServiceDataSingleton.isServiceRunning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton("No") { _, _ -> }
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||||
|
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||||
|
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
AnimeServiceDataSingleton.isServiceRunning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun getSimpleCache(context: Context): SimpleCache {
|
||||||
|
return if (simpleCache == null) {
|
||||||
|
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||||
|
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||||
|
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
|
||||||
|
simpleCache!!
|
||||||
|
} else {
|
||||||
|
simpleCache!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
return ActivityCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package ani.dantotsu.home
|
|||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -47,8 +48,10 @@ import kotlin.math.min
|
|||||||
class AnimeFragment : Fragment() {
|
class AnimeFragment : Fragment() {
|
||||||
private var _binding: FragmentAnimeBinding? = null
|
private var _binding: FragmentAnimeBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
private lateinit var animePageAdapter: AnimePageAdapter
|
||||||
|
|
||||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
private var uiSettings: UserInterfaceSettings =
|
||||||
|
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
|
||||||
val model: AnilistAnimeViewModel by activityViewModels()
|
val model: AnilistAnimeViewModel by activityViewModels()
|
||||||
|
|
||||||
@@ -94,7 +97,7 @@ class AnimeFragment : Fragment() {
|
|||||||
|
|
||||||
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||||
|
|
||||||
val animePageAdapter = AnimePageAdapter()
|
animePageAdapter = AnimePageAdapter()
|
||||||
|
|
||||||
var loading = true
|
var loading = true
|
||||||
if (model.notSet) {
|
if (model.notSet) {
|
||||||
@@ -224,7 +227,8 @@ class AnimeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.animePageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
binding.animePageScrollTop.translationY =
|
||||||
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +268,8 @@ class AnimeFragment : Fragment() {
|
|||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.loadTrending(1)
|
model.loadTrending(1)
|
||||||
model.loadUpdated()
|
model.loadUpdated()
|
||||||
model.loadPopular("ANIME", sort = Anilist.sortBy[1])
|
model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("popular_list", false))
|
||||||
}
|
}
|
||||||
live.postValue(false)
|
live.postValue(false)
|
||||||
_binding?.animeRefresh?.isRefreshing = false
|
_binding?.animeRefresh?.isRefreshing = false
|
||||||
@@ -275,6 +280,11 @@ class AnimeFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||||
|
if (animePageAdapter.trendingViewPager != null) {
|
||||||
|
binding.root.requestApplyInsets()
|
||||||
|
binding.root.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -15,13 +17,15 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.media.GenreActivity
|
|
||||||
import ani.dantotsu.MediaPageTransformer
|
import ani.dantotsu.MediaPageTransformer
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.CalendarActivity
|
import ani.dantotsu.media.CalendarActivity
|
||||||
|
import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.SearchActivity
|
import ani.dantotsu.media.SearchActivity
|
||||||
import ani.dantotsu.px
|
import ani.dantotsu.px
|
||||||
@@ -31,6 +35,8 @@ import ani.dantotsu.setSlideUp
|
|||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
|
||||||
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
||||||
val ready = MutableLiveData(false)
|
val ready = MutableLiveData(false)
|
||||||
@@ -38,10 +44,12 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
private var trendHandler: Handler? = null
|
private var trendHandler: Handler? = null
|
||||||
private lateinit var trendRun: Runnable
|
private lateinit var trendRun: Runnable
|
||||||
var trendingViewPager: ViewPager2? = null
|
var trendingViewPager: ViewPager2? = null
|
||||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
private var uiSettings: UserInterfaceSettings =
|
||||||
|
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
|
||||||
val binding = ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return AnimePageViewHolder(binding)
|
return AnimePageViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +57,25 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
binding = holder.binding
|
binding = holder.binding
|
||||||
trendingViewPager = binding.animeTrendingViewPager
|
trendingViewPager = binding.animeTrendingViewPager
|
||||||
|
|
||||||
|
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
||||||
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
|
val materialCardView =
|
||||||
|
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||||
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
|
val color = typedValue.data
|
||||||
|
|
||||||
|
|
||||||
|
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getBoolean("colorOverflow", false) ?: false
|
||||||
|
if (!colorOverflow) {
|
||||||
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||||
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -71,7 +98,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.animeUserAvatar.setSafeOnClickListener {
|
binding.animeUserAvatar.setSafeOnClickListener {
|
||||||
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
val dialogFragment =
|
||||||
|
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
|
||||||
|
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
@@ -101,9 +130,17 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE
|
binding.animeIncludeList.visibility =
|
||||||
|
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
binding.animeIncludeList.isChecked = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getBoolean("popular_list", true) ?: true
|
||||||
|
|
||||||
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||||
onIncludeListClick.invoke(isChecked)
|
onIncludeListClick.invoke(isChecked)
|
||||||
|
|
||||||
|
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||||
|
?.putBoolean("popular_list", isChecked)?.apply()
|
||||||
}
|
}
|
||||||
if (ready.value == false)
|
if (ready.value == false)
|
||||||
ready.postValue(true)
|
ready.postValue(true)
|
||||||
@@ -128,7 +165,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
|
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
binding.animeTrendingViewPager.currentItem = binding.animeTrendingViewPager.currentItem + 1
|
binding.animeTrendingViewPager.currentItem =
|
||||||
|
binding.animeTrendingViewPager.currentItem + 1
|
||||||
}
|
}
|
||||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
@@ -140,22 +178,30 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.animeTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.animeTrendingViewPager.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
|
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||||
binding.animeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.animeListContainer.layoutAnimation =
|
||||||
binding.animeSeasonsCont.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
|
binding.animeSeasonsCont.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateRecent(adaptor: MediaAdaptor) {
|
fun updateRecent(adaptor: MediaAdaptor) {
|
||||||
binding.animeUpdatedProgressBar.visibility = View.GONE
|
binding.animeUpdatedProgressBar.visibility = View.GONE
|
||||||
binding.animeUpdatedRecyclerView.adapter = adaptor
|
binding.animeUpdatedRecyclerView.adapter = adaptor
|
||||||
binding.animeUpdatedRecyclerView.layoutManager =
|
binding.animeUpdatedRecyclerView.layoutManager =
|
||||||
LinearLayoutManager(binding.animeUpdatedRecyclerView.context, LinearLayoutManager.HORIZONTAL, false)
|
LinearLayoutManager(
|
||||||
|
binding.animeUpdatedRecyclerView.context,
|
||||||
|
LinearLayoutManager.HORIZONTAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||||
|
|
||||||
binding.animeRecently.visibility = View.VISIBLE
|
binding.animeRecently.visibility = View.VISIBLE
|
||||||
binding.animeRecently.startAnimation(setSlideUp(uiSettings))
|
binding.animeRecently.startAnimation(setSlideUp(uiSettings))
|
||||||
binding.animeUpdatedRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.animeUpdatedRecyclerView.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
binding.animePopular.visibility = View.VISIBLE
|
binding.animePopular.visibility = View.VISIBLE
|
||||||
binding.animePopular.startAnimation(setSlideUp(uiSettings))
|
binding.animePopular.startAnimation(setSlideUp(uiSettings))
|
||||||
}
|
}
|
||||||
@@ -163,8 +209,10 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
fun updateAvatar() {
|
fun updateAvatar() {
|
||||||
if (Anilist.avatar != null && ready.value == true) {
|
if (Anilist.avatar != null && ready.value == true) {
|
||||||
binding.animeUserAvatar.loadImage(Anilist.avatar)
|
binding.animeUserAvatar.loadImage(Anilist.avatar)
|
||||||
|
binding.animeUserAvatar.imageTintList = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ import ani.dantotsu.loadData
|
|||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
|
||||||
import ani.dantotsu.media.user.ListActivity
|
import ani.dantotsu.media.user.ListActivity
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.setSlideIn
|
import ani.dantotsu.setSlideIn
|
||||||
import ani.dantotsu.setSlideUp
|
import ani.dantotsu.setSlideUp
|
||||||
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -52,7 +52,11 @@ class HomeFragment : Fragment() {
|
|||||||
private var _binding: FragmentHomeBinding? = null
|
private var _binding: FragmentHomeBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -96,18 +100,24 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
||||||
binding.homeUserDataContainer.visibility = View.VISIBLE
|
binding.homeUserDataContainer.visibility = View.VISIBLE
|
||||||
binding.homeUserDataContainer.layoutAnimation = LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
|
binding.homeUserDataContainer.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
|
||||||
binding.homeAnimeList.visibility = View.VISIBLE
|
binding.homeAnimeList.visibility = View.VISIBLE
|
||||||
binding.homeMangaList.visibility = View.VISIBLE
|
binding.homeMangaList.visibility = View.VISIBLE
|
||||||
binding.homeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.homeListContainer.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
snackString(currContext()?.getString(R.string.please_reload))
|
snackString(currContext()?.getString(R.string.please_reload))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
||||||
SettingsDialogFragment().show(parentFragmentManager, "dialog")
|
val dialogFragment =
|
||||||
|
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
|
||||||
|
dialogFragment.show(
|
||||||
|
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
|
||||||
|
"dialog"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -118,17 +128,17 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
var reached = false
|
var reached = false
|
||||||
val duration = (uiSettings.animationSpeed * 200).toLong()
|
val duration = (uiSettings.animationSpeed * 200).toLong()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||||
reached = true
|
reached = true
|
||||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration).start()
|
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||||
|
.start()
|
||||||
} else {
|
} else {
|
||||||
if (reached) {
|
if (reached) {
|
||||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration).start()
|
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||||
}
|
.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +148,13 @@ class HomeFragment : Fragment() {
|
|||||||
if (displayCutout != null) {
|
if (displayCutout != null) {
|
||||||
if (displayCutout.boundingRects.size > 0) {
|
if (displayCutout.boundingRects.size > 0) {
|
||||||
height =
|
height =
|
||||||
max(statusBarHeight, min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height()))
|
max(
|
||||||
|
statusBarHeight,
|
||||||
|
min(
|
||||||
|
displayCutout.boundingRects[0].width(),
|
||||||
|
displayCutout.boundingRects[0].height()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +205,8 @@ class HomeFragment : Fragment() {
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
recyclerView.visibility = View.VISIBLE
|
recyclerView.visibility = View.VISIBLE
|
||||||
recyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
recyclerView.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
empty.visibility = View.VISIBLE
|
empty.visibility = View.VISIBLE
|
||||||
@@ -313,7 +330,8 @@ class HomeFragment : Fragment() {
|
|||||||
live.observe(viewLifecycleOwner) {
|
live.observe(viewLifecycleOwner) {
|
||||||
if (it) {
|
if (it) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
uiSettings =
|
||||||
|
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
//Get userData First
|
//Get userData First
|
||||||
getUserId(requireContext()) {
|
getUserId(requireContext()) {
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ class LoginFragment : Fragment() {
|
|||||||
private var _binding: FragmentLoginBinding? = null
|
private var _binding: FragmentLoginBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = FragmentLoginBinding.inflate(layoutInflater, container, false)
|
_binding = FragmentLoginBinding.inflate(layoutInflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -24,5 +28,6 @@ class LoginFragment : Fragment() {
|
|||||||
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
|
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
|
||||||
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
|
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
|
||||||
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
|
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
|
||||||
|
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package ani.dantotsu.home
|
|||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -43,12 +44,18 @@ import kotlin.math.min
|
|||||||
class MangaFragment : Fragment() {
|
class MangaFragment : Fragment() {
|
||||||
private var _binding: FragmentMangaBinding? = null
|
private var _binding: FragmentMangaBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
private lateinit var mangaPageAdapter: MangaPageAdapter
|
||||||
|
|
||||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
private var uiSettings: UserInterfaceSettings =
|
||||||
|
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
|
||||||
val model: AnilistMangaViewModel by activityViewModels()
|
val model: AnilistMangaViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = FragmentMangaBinding.inflate(inflater, container, false)
|
_binding = FragmentMangaBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -85,7 +92,7 @@ class MangaFragment : Fragment() {
|
|||||||
|
|
||||||
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||||
|
|
||||||
val mangaPageAdapter = MangaPageAdapter()
|
mangaPageAdapter = MangaPageAdapter()
|
||||||
var loading = true
|
var loading = true
|
||||||
if (model.notSet) {
|
if (model.notSet) {
|
||||||
model.notSet = false
|
model.notSet = false
|
||||||
@@ -100,7 +107,8 @@ class MangaFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
|
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
|
||||||
val progressAdaptor = ProgressAdapter(searched = model.searched)
|
val progressAdaptor = ProgressAdapter(searched = model.searched)
|
||||||
binding.mangaPageRecyclerView.adapter = ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
binding.mangaPageRecyclerView.adapter =
|
||||||
|
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
||||||
val layout = LinearLayoutManager(requireContext())
|
val layout = LinearLayoutManager(requireContext())
|
||||||
binding.mangaPageRecyclerView.layoutManager = layout
|
binding.mangaPageRecyclerView.layoutManager = layout
|
||||||
|
|
||||||
@@ -177,7 +185,8 @@ class MangaFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.mangaPageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
binding.mangaPageScrollTop.translationY =
|
||||||
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +242,8 @@ class MangaFragment : Fragment() {
|
|||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.loadTrending()
|
model.loadTrending()
|
||||||
model.loadTrendingNovel()
|
model.loadTrendingNovel()
|
||||||
model.loadPopular("MANGA", sort = Anilist.sortBy[1])
|
model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("popular_list", false) )
|
||||||
}
|
}
|
||||||
live.postValue(false)
|
live.postValue(false)
|
||||||
_binding?.mangaRefresh?.isRefreshing = false
|
_binding?.mangaRefresh?.isRefreshing = false
|
||||||
@@ -244,6 +254,11 @@ class MangaFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||||
|
//make sure mangaPageAdapter is initialized
|
||||||
|
if (mangaPageAdapter.trendingViewPager != null) {
|
||||||
|
binding.root.requestApplyInsets()
|
||||||
|
binding.root.requestLayout()
|
||||||
|
}
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -15,12 +17,14 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.media.GenreActivity
|
|
||||||
import ani.dantotsu.MediaPageTransformer
|
import ani.dantotsu.MediaPageTransformer
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.SearchActivity
|
import ani.dantotsu.media.SearchActivity
|
||||||
import ani.dantotsu.px
|
import ani.dantotsu.px
|
||||||
@@ -30,6 +34,8 @@ import ani.dantotsu.setSlideUp
|
|||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
|
||||||
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
||||||
val ready = MutableLiveData(false)
|
val ready = MutableLiveData(false)
|
||||||
@@ -37,10 +43,12 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
private var trendHandler: Handler? = null
|
private var trendHandler: Handler? = null
|
||||||
private lateinit var trendRun: Runnable
|
private lateinit var trendRun: Runnable
|
||||||
var trendingViewPager: ViewPager2? = null
|
var trendingViewPager: ViewPager2? = null
|
||||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
private var uiSettings: UserInterfaceSettings =
|
||||||
|
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
|
||||||
val binding = ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return MangaPageViewHolder(binding)
|
return MangaPageViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +56,25 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
binding = holder.binding
|
binding = holder.binding
|
||||||
trendingViewPager = binding.mangaTrendingViewPager
|
trendingViewPager = binding.mangaTrendingViewPager
|
||||||
|
|
||||||
|
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
||||||
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
|
val materialCardView =
|
||||||
|
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||||
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
|
val color = typedValue.data
|
||||||
|
|
||||||
|
|
||||||
|
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getBoolean("colorOverflow", false) ?: false
|
||||||
|
if (!colorOverflow) {
|
||||||
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||||
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -66,7 +93,9 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
binding.mangaUserAvatar.setSafeOnClickListener {
|
||||||
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
val dialogFragment =
|
||||||
|
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
|
||||||
|
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaSearchBar.setEndIconOnClickListener {
|
binding.mangaSearchBar.setEndIconOnClickListener {
|
||||||
@@ -94,11 +123,18 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE
|
binding.mangaIncludeList.visibility =
|
||||||
|
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
binding.mangaIncludeList.isChecked = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getBoolean("popular_list", true) ?: true
|
||||||
|
|
||||||
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||||
onIncludeListClick.invoke(isChecked)
|
onIncludeListClick.invoke(isChecked)
|
||||||
}
|
|
||||||
|
|
||||||
|
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||||
|
?.putBoolean("popular_list", isChecked)?.apply()
|
||||||
|
}
|
||||||
if (ready.value == false)
|
if (ready.value == false)
|
||||||
ready.postValue(true)
|
ready.postValue(true)
|
||||||
}
|
}
|
||||||
@@ -119,7 +155,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
binding.mangaTrendingViewPager.currentItem = binding.mangaTrendingViewPager.currentItem + 1
|
binding.mangaTrendingViewPager.currentItem =
|
||||||
|
binding.mangaTrendingViewPager.currentItem + 1
|
||||||
}
|
}
|
||||||
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
@@ -131,21 +168,28 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.mangaTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.mangaTrendingViewPager.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
|
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||||
binding.mangaListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.mangaListContainer.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNovel(adaptor: MediaAdaptor) {
|
fun updateNovel(adaptor: MediaAdaptor) {
|
||||||
binding.mangaNovelProgressBar.visibility = View.GONE
|
binding.mangaNovelProgressBar.visibility = View.GONE
|
||||||
binding.mangaNovelRecyclerView.adapter = adaptor
|
binding.mangaNovelRecyclerView.adapter = adaptor
|
||||||
binding.mangaNovelRecyclerView.layoutManager =
|
binding.mangaNovelRecyclerView.layoutManager =
|
||||||
LinearLayoutManager(binding.mangaNovelRecyclerView.context, LinearLayoutManager.HORIZONTAL, false)
|
LinearLayoutManager(
|
||||||
|
binding.mangaNovelRecyclerView.context,
|
||||||
|
LinearLayoutManager.HORIZONTAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||||
|
|
||||||
binding.mangaNovel.visibility = View.VISIBLE
|
binding.mangaNovel.visibility = View.VISIBLE
|
||||||
binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
|
binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
|
||||||
binding.mangaNovelRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.mangaNovelRecyclerView.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||||
binding.mangaPopular.visibility = View.VISIBLE
|
binding.mangaPopular.visibility = View.VISIBLE
|
||||||
binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
|
binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
|
||||||
}
|
}
|
||||||
@@ -153,8 +197,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
fun updateAvatar() {
|
fun updateAvatar() {
|
||||||
if (Anilist.avatar != null && ready.value == true) {
|
if (Anilist.avatar != null && ready.value == true) {
|
||||||
binding.mangaUserAvatar.loadImage(Anilist.avatar)
|
binding.mangaUserAvatar.loadImage(Anilist.avatar)
|
||||||
|
binding.mangaUserAvatar.imageTintList = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,124 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.addCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.doOnAttach
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.ZoomOutPageTransformer
|
||||||
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
||||||
import ani.dantotsu.isOnline
|
import ani.dantotsu.download.anime.OfflineAnimeFragment
|
||||||
|
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.offline.OfflineFragment
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.others.LangSet
|
||||||
|
import ani.dantotsu.selectedOption
|
||||||
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
|
|
||||||
class NoInternet : AppCompatActivity() {
|
class NoInternet : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityNoInternetBinding
|
||||||
|
lateinit var bottomBar: AnimatedBottomBar
|
||||||
|
private var uiSettings = UserInterfaceSettings()
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
val binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||||
topMargin = statusBarHeight
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
|
||||||
|
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||||
|
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||||
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||||
|
backgroundDrawable.setColor(semiTransparentColor)
|
||||||
|
_bottomBar.background = backgroundDrawable
|
||||||
|
}
|
||||||
|
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("colorOverflow", false)
|
||||||
|
if (!colorOverflow) {
|
||||||
|
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var doubleBackToExitPressedOnce = false
|
||||||
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
|
if (doubleBackToExitPressedOnce) {
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
doubleBackToExitPressedOnce = true
|
||||||
|
snackString(this@NoInternet.getString(R.string.back_to_exit))
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed(
|
||||||
|
{ doubleBackToExitPressedOnce = false },
|
||||||
|
2000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root.doOnAttach {
|
||||||
|
initActivity(this)
|
||||||
|
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||||
|
selectedOption = uiSettings.defaultStartUpTab
|
||||||
|
|
||||||
|
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = navBarHeight
|
bottomMargin = navBarHeight
|
||||||
}
|
}
|
||||||
binding.refreshButton.setOnClickListener {
|
}
|
||||||
if (isOnline(this)) {
|
val navbar = binding.includedNavbar.navbar
|
||||||
startMainActivity(this)
|
ani.dantotsu.bottomBar = navbar
|
||||||
|
navbar.visibility = View.VISIBLE
|
||||||
|
val mainViewPager = binding.viewpager
|
||||||
|
mainViewPager.isUserInputEnabled = false
|
||||||
|
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||||
|
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||||
|
navbar.setOnTabSelectListener(object :
|
||||||
|
AnimatedBottomBar.OnTabSelectListener {
|
||||||
|
override fun onTabSelected(
|
||||||
|
lastIndex: Int,
|
||||||
|
lastTab: AnimatedBottomBar.Tab?,
|
||||||
|
newIndex: Int,
|
||||||
|
newTab: AnimatedBottomBar.Tab
|
||||||
|
) {
|
||||||
|
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||||
|
selectedOption = newIndex
|
||||||
|
mainViewPager.setCurrentItem(newIndex, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
navbar.selectTabAt(selectedOption)
|
||||||
|
|
||||||
|
//supportFragmentManager.beginTransaction().replace(binding.fragmentContainer.id, OfflineFragment()).commit()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||||
|
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = 3
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return when (position) {
|
||||||
|
0 -> OfflineAnimeFragment()
|
||||||
|
2 -> OfflineMangaFragment()
|
||||||
|
else -> OfflineFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,17 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.EmptyAdapter
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityAuthorBinding
|
import ani.dantotsu.databinding.ActivityAuthorBinding
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.px
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -28,6 +36,8 @@ class AuthorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,27 @@ package ani.dantotsu.media
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityListBinding
|
import ani.dantotsu.databinding.ActivityListBinding
|
||||||
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,10 +38,55 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityListBinding.inflate(layoutInflater)
|
binding = ActivityListBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||||
|
val primaryColor = typedValue.data
|
||||||
|
val typedValue2 = TypedValue()
|
||||||
|
theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorOnBackground,
|
||||||
|
typedValue2,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val titleTextColor = typedValue2.data
|
||||||
|
val typedValue3 = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||||
|
val primaryTextColor = typedValue3.data
|
||||||
|
val typedValue4 = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
|
||||||
|
val secondaryTextColor = typedValue4.data
|
||||||
|
|
||||||
|
window.statusBarColor = primaryColor
|
||||||
|
window.navigationBarColor = primaryColor
|
||||||
|
binding.listTabLayout.setBackgroundColor(primaryColor)
|
||||||
|
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||||
|
binding.listTitle.setTextColor(primaryTextColor)
|
||||||
|
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||||
|
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||||
|
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
if (!uiSettings.immersiveMode) {
|
||||||
|
this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||||
|
binding.root.fitsSystemWindows = true
|
||||||
|
|
||||||
|
} else {
|
||||||
|
binding.root.fitsSystemWindows = false
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
window.setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
|
)
|
||||||
|
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = statusBarHeight
|
||||||
|
bottomMargin = navBarHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
|
||||||
binding.listTitle.setText(R.string.release_calendar)
|
binding.listTitle.setText(R.string.release_calendar)
|
||||||
binding.listSort.visibility = View.GONE
|
binding.listSort.visibility = View.GONE
|
||||||
|
|
||||||
@@ -38,6 +94,7 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ class CharacterAdapter(
|
|||||||
private val characterList: ArrayList<Character>
|
private val characterList: ArrayList<Character>
|
||||||
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
|
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
|
||||||
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return CharacterViewHolder(binding)
|
return CharacterViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
private val uiSettings =
|
||||||
|
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||||
@@ -38,16 +40,23 @@ class CharacterAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = characterList.size
|
override fun getItemCount(): Int = characterList.size
|
||||||
inner class CharacterViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class CharacterViewHolder(val binding: ItemCharacterBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
val char = characterList[bindingAdapterPosition]
|
val char = characterList[bindingAdapterPosition]
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
itemView.context,
|
itemView.context,
|
||||||
Intent(itemView.context, CharacterDetailsActivity::class.java).putExtra("character", char as Serializable),
|
Intent(
|
||||||
|
itemView.context,
|
||||||
|
CharacterDetailsActivity::class.java
|
||||||
|
).putExtra("character", char as Serializable),
|
||||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
itemView.context as Activity,
|
itemView.context as Activity,
|
||||||
Pair.create(binding.itemCompactImage, ViewCompat.getTransitionName(binding.itemCompactImage)!!),
|
Pair.create(
|
||||||
|
binding.itemCompactImage,
|
||||||
|
ViewCompat.getTransitionName(binding.itemCompactImage)!!
|
||||||
|
),
|
||||||
).toBundle()
|
).toBundle()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,20 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityCharacterBinding
|
import ani.dantotsu.databinding.ActivityCharacterBinding
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.loadData
|
||||||
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.px
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -33,14 +42,18 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status)
|
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.status)
|
||||||
|
|
||||||
val banner = if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
|
val banner =
|
||||||
|
if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
|
||||||
|
|
||||||
banner.updateLayoutParams { height += statusBarHeight }
|
banner.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||||
@@ -57,7 +70,13 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
binding.characterTitle.text = character.name
|
binding.characterTitle.text = character.name
|
||||||
banner.loadImage(character.banner)
|
banner.loadImage(character.banner)
|
||||||
binding.characterCoverImage.loadImage(character.image)
|
binding.characterCoverImage.loadImage(character.image)
|
||||||
binding.characterCoverImage.setOnLongClickListener { ImageViewDialog.newInstance(this, character.name, character.image) }
|
binding.characterCoverImage.setOnLongClickListener {
|
||||||
|
ImageViewDialog.newInstance(
|
||||||
|
this,
|
||||||
|
character.name,
|
||||||
|
character.image
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
model.getCharacter().observe(this) {
|
model.getCharacter().observe(this) {
|
||||||
if (it != null && !loaded) {
|
if (it != null && !loaded) {
|
||||||
@@ -69,7 +88,8 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
val roles = character.roles
|
val roles = character.roles
|
||||||
if (roles != null) {
|
if (roles != null) {
|
||||||
val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true)
|
val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true)
|
||||||
val concatAdaptor = ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor)
|
val concatAdaptor =
|
||||||
|
ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor)
|
||||||
|
|
||||||
val gridSize = (screenWidth / 124f).toInt()
|
val gridSize = (screenWidth / 124f).toInt()
|
||||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||||
@@ -114,16 +134,19 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
binding.characterCover.scaleY = 1f * cap
|
binding.characterCover.scaleY = 1f * cap
|
||||||
binding.characterCover.cardElevation = 32f * cap
|
binding.characterCover.cardElevation = 32f * cap
|
||||||
|
|
||||||
binding.characterCover.visibility = if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
binding.characterCover.visibility =
|
||||||
|
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
if (percentage >= percent && !isCollapsed) {
|
if (percentage >= percent && !isCollapsed) {
|
||||||
isCollapsed = true
|
isCollapsed = true
|
||||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.nav_bg)
|
||||||
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
||||||
}
|
}
|
||||||
if (percentage <= percent && isCollapsed) {
|
if (percentage <= percent && isCollapsed) {
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status)
|
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.status)
|
||||||
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
|||||||
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
|
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
|
||||||
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
|
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
|
||||||
val binding = ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return GenreViewHolder(binding)
|
return GenreViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,11 +33,13 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
|||||||
} else "") + "\n" + character.description
|
} else "") + "\n" + character.description
|
||||||
|
|
||||||
binding.characterDesc.isTextSelectable
|
binding.characterDesc.isTextSelectable
|
||||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(SpoilerPlugin()).build()
|
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||||
|
.usePlugin(SpoilerPlugin()).build()
|
||||||
markWon.setMarkdown(binding.characterDesc, desc)
|
markWon.setMarkdown(binding.characterDesc, desc)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,9 @@ import ani.dantotsu.databinding.ActivityGenreBinding
|
|||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -25,6 +27,8 @@ class GenreActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityGenreBinding.inflate(layoutInflater)
|
binding = ActivityGenreBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
@@ -46,7 +50,8 @@ class GenreActivity : AppCompatActivity() {
|
|||||||
model.doneListener?.invoke()
|
model.doneListener?.invoke()
|
||||||
}
|
}
|
||||||
binding.mediaInfoGenresRecyclerView.adapter = adapter
|
binding.mediaInfoGenresRecyclerView.adapter = adapter
|
||||||
binding.mediaInfoGenresRecyclerView.layoutManager = GridLayoutManager(this, (screenWidth / 156f).toInt())
|
binding.mediaInfoGenresRecyclerView.layoutManager =
|
||||||
|
GridLayoutManager(this, (screenWidth / 156f).toInt())
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {
|
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ class GenreAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = genres.size
|
override fun getItemCount(): Int = genres.size
|
||||||
inner class GenreViewHolder(val binding: ItemGenreBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class GenreViewHolder(val binding: ItemGenreBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.connections.anilist.api.MediaEdge
|
import ani.dantotsu.connections.anilist.api.MediaEdge
|
||||||
import ani.dantotsu.connections.anilist.api.MediaList
|
import ani.dantotsu.connections.anilist.api.MediaList
|
||||||
@@ -22,7 +23,7 @@ data class Media(
|
|||||||
val userPreferredName: String,
|
val userPreferredName: String,
|
||||||
|
|
||||||
var cover: String? = null,
|
var cover: String? = null,
|
||||||
val banner: String? = null,
|
var banner: String? = null,
|
||||||
var relation: String? = null,
|
var relation: String? = null,
|
||||||
var popularity: Int? = null,
|
var popularity: Int? = null,
|
||||||
|
|
||||||
@@ -117,6 +118,20 @@ data class Media(
|
|||||||
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
|
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun emptyMedia() = Media(
|
||||||
|
id = 0,
|
||||||
|
name = "No media found",
|
||||||
|
nameRomaji = "No media found",
|
||||||
|
userPreferredName = "",
|
||||||
|
isAdult = false,
|
||||||
|
isFav = false,
|
||||||
|
isListPrivate = false,
|
||||||
|
userScore = 0,
|
||||||
|
userStatus = "",
|
||||||
|
format = "",
|
||||||
|
)
|
||||||
|
|
||||||
object MediaSingleton {
|
object MediaSingleton {
|
||||||
var media: Media? = null
|
var media: Media? = null
|
||||||
|
var bitmap: Bitmap? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.util.Pair
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -37,13 +44,35 @@ class MediaAdaptor(
|
|||||||
private val viewPager: ViewPager2? = null,
|
private val viewPager: ViewPager2? = null,
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
private val uiSettings =
|
||||||
|
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
0 -> MediaViewHolder(ItemMediaCompactBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
0 -> MediaViewHolder(
|
||||||
1 -> MediaLargeViewHolder(ItemMediaLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
ItemMediaCompactBinding.inflate(
|
||||||
2 -> MediaPageViewHolder(ItemMediaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
1 -> MediaLargeViewHolder(
|
||||||
|
ItemMediaLargeBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
2 -> MediaPageViewHolder(
|
||||||
|
ItemMediaPageBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
3 -> MediaPageSmallViewHolder(
|
3 -> MediaPageSmallViewHolder(
|
||||||
ItemMediaPageSmallBinding.inflate(
|
ItemMediaPageSmallBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
@@ -51,6 +80,7 @@ class MediaAdaptor(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +95,12 @@ class MediaAdaptor(
|
|||||||
val media = mediaList?.getOrNull(position)
|
val media = mediaList?.getOrNull(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
?: 0) else media.userScore) / 10.0).toString()
|
||||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||||
b.root.context,
|
b.root.context,
|
||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
@@ -100,29 +132,37 @@ class MediaAdaptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
val b = (holder as MediaLargeViewHolder).binding
|
val b = (holder as MediaLargeViewHolder).binding
|
||||||
setAnimation(activity, b.root, uiSettings)
|
setAnimation(activity, b.root, uiSettings)
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400)
|
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
|
||||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
?: 0) else media.userScore) / 10.0).toString()
|
||||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||||
b.root.context,
|
b.root.context,
|
||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
)
|
)
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.episode_plural)
|
||||||
else currActivity()!!.getString(R.string.episode_singular)
|
else currActivity()!!.getString(R.string.episode_singular)
|
||||||
b.itemCompactTotal.text =
|
b.itemCompactTotal.text =
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
?: "??").toString()) else (media.anime.totalEpisodes
|
||||||
|
?: "??").toString()
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.chapter_plural)
|
||||||
else currActivity()!!.getString(R.string.chapter_singular)
|
else currActivity()!!.getString(R.string.chapter_singular)
|
||||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||||
}
|
}
|
||||||
@@ -133,6 +173,7 @@ class MediaAdaptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> {
|
2 -> {
|
||||||
val b = (holder as MediaPageViewHolder).binding
|
val b = (holder as MediaPageViewHolder).binding
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
@@ -145,7 +186,8 @@ class MediaAdaptor(
|
|||||||
AccelerateDecelerateInterpolator()
|
AccelerateDecelerateInterpolator()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
val banner =
|
||||||
|
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||||
val context = b.itemCompactBanner.context
|
val context = b.itemCompactBanner.context
|
||||||
if (!(context as Activity).isDestroyed)
|
if (!(context as Activity).isDestroyed)
|
||||||
Glide.with(context as Context)
|
Glide.with(context as Context)
|
||||||
@@ -153,22 +195,29 @@ class MediaAdaptor(
|
|||||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||||
.into(banner)
|
.into(banner)
|
||||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
?: 0) else media.userScore) / 10.0).toString()
|
||||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||||
b.root.context,
|
b.root.context,
|
||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
)
|
)
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.episode_plural)
|
||||||
else currActivity()!!.getString(R.string.episode_singular)
|
else currActivity()!!.getString(R.string.episode_singular)
|
||||||
b.itemCompactTotal.text =
|
b.itemCompactTotal.text =
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
?: "??").toString()) else (media.anime.totalEpisodes
|
||||||
|
?: "??").toString()
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
b.itemTotal.text =" " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.chapter_plural)
|
||||||
else currActivity()!!.getString(R.string.chapter_singular)
|
else currActivity()!!.getString(R.string.chapter_singular)
|
||||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||||
}
|
}
|
||||||
@@ -180,6 +229,7 @@ class MediaAdaptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
3 -> {
|
3 -> {
|
||||||
val b = (holder as MediaPageSmallViewHolder).binding
|
val b = (holder as MediaPageSmallViewHolder).binding
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
@@ -192,7 +242,8 @@ class MediaAdaptor(
|
|||||||
AccelerateDecelerateInterpolator()
|
AccelerateDecelerateInterpolator()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
val banner =
|
||||||
|
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||||
val context = b.itemCompactBanner.context
|
val context = b.itemCompactBanner.context
|
||||||
if (!(context as Activity).isDestroyed)
|
if (!(context as Activity).isDestroyed)
|
||||||
Glide.with(context as Context)
|
Glide.with(context as Context)
|
||||||
@@ -200,10 +251,12 @@ class MediaAdaptor(
|
|||||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||||
.into(banner)
|
.into(banner)
|
||||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
?: 0) else media.userScore) / 10.0).toString()
|
||||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||||
b.root.context,
|
b.root.context,
|
||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
@@ -218,13 +271,18 @@ class MediaAdaptor(
|
|||||||
}
|
}
|
||||||
b.itemCompactStatus.text = media.status ?: ""
|
b.itemCompactStatus.text = media.status ?: ""
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.episode_plural)
|
||||||
else currActivity()!!.getString(R.string.episode_singular)
|
else currActivity()!!.getString(R.string.episode_singular)
|
||||||
b.itemCompactTotal.text =
|
b.itemCompactTotal.text =
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
?: "??").toString()) else (media.anime.totalEpisodes
|
||||||
|
?: "??").toString()
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.chapter_plural)
|
||||||
else currActivity()!!.getString(R.string.chapter_singular)
|
else currActivity()!!.getString(R.string.chapter_singular)
|
||||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||||
}
|
}
|
||||||
@@ -245,61 +303,163 @@ class MediaAdaptor(
|
|||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) : RecyclerView.ViewHolder(binding.root) {
|
fun randomOptionClick() {
|
||||||
|
val media = if (!mediaList.isNullOrEmpty()) {
|
||||||
|
mediaList.random()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
media?.let {
|
||||||
|
val index = mediaList?.indexOf(it) ?: -1
|
||||||
|
clicked(index, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
if (matchParent) itemView.updateLayoutParams { width = -1 }
|
if (matchParent) itemView.updateLayoutParams { width = -1 }
|
||||||
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
itemView.setSafeOnClickListener {
|
||||||
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
itemView.setSafeOnClickListener {
|
||||||
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
binding.itemCompactImage.setSafeOnClickListener {
|
||||||
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
itemView.setOnTouchListener { _, _ -> true }
|
itemView.setOnTouchListener { _, _ -> true }
|
||||||
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
binding.itemCompactImage.setSafeOnClickListener {
|
||||||
binding.itemCompactTitleContainer.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.itemCompactTitleContainer.setSafeOnClickListener {
|
||||||
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
itemView.setOnTouchListener { _, _ -> true }
|
itemView.setOnTouchListener { _, _ -> true }
|
||||||
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clicked(position: Int) {
|
fun clicked(position: Int, itemCompactImage: ImageView?, bitmap: Bitmap? = null) {
|
||||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
|
if (bitmap != null) MediaSingleton.bitmap = bitmap
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
activity,
|
activity,
|
||||||
Intent(activity, MediaDetailsActivity::class.java).putExtra(
|
Intent(activity, MediaDetailsActivity::class.java).putExtra(
|
||||||
"media",
|
"media",
|
||||||
media as Serializable
|
media as Serializable
|
||||||
), null
|
),
|
||||||
|
if (itemCompactImage != null) {
|
||||||
|
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
|
activity,
|
||||||
|
Pair.create(
|
||||||
|
itemCompactImage,
|
||||||
|
ViewCompat.getTransitionName(activity.findViewById(R.id.itemCompactImage))!!
|
||||||
|
),
|
||||||
|
).toBundle()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun longClicked(position: Int): Boolean {
|
fun longClicked(position: Int): Boolean {
|
||||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||||
val media = mediaList?.get(position) ?: return false
|
val media = mediaList?.get(position) ?: return false
|
||||||
if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
|
if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
|
||||||
MediaListDialogSmallFragment.newInstance(media).show(activity.supportFragmentManager, "list")
|
MediaListDialogSmallFragment.newInstance(media)
|
||||||
|
.show(activity.supportFragmentManager, "list")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getBitmapFromImageView(imageView: ImageView): Bitmap? {
|
||||||
|
val drawable = imageView.drawable ?: return null
|
||||||
|
|
||||||
|
// If the drawable is a BitmapDrawable, then just get the bitmap
|
||||||
|
if (drawable is BitmapDrawable) {
|
||||||
|
return drawable.bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a bitmap with the same dimensions as the drawable
|
||||||
|
val bitmap = Bitmap.createBitmap(
|
||||||
|
drawable.intrinsicWidth,
|
||||||
|
drawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw the drawable onto the bitmap
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resizeBitmap(source: Bitmap?, maxDimension: Int): Bitmap? {
|
||||||
|
if (source == null) return null
|
||||||
|
val width = source.width
|
||||||
|
val height = source.height
|
||||||
|
val newWidth: Int
|
||||||
|
val newHeight: Int
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
newWidth = maxDimension
|
||||||
|
newHeight = (height * (maxDimension.toFloat() / width)).toInt()
|
||||||
|
} else {
|
||||||
|
newHeight = maxDimension
|
||||||
|
newWidth = (width * (maxDimension.toFloat() / height)).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bitmap.createScaledBitmap(source, newWidth, newHeight, true)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package ani.dantotsu.media
|
|||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
@@ -31,15 +32,15 @@ import ani.dantotsu.R
|
|||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.ZoomOutPageTransformer
|
import ani.dantotsu.ZoomOutPageTransformer
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
|
||||||
import ani.dantotsu.copyToClipboard
|
import ani.dantotsu.copyToClipboard
|
||||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment
|
import ani.dantotsu.media.manga.MangaReadFragment
|
||||||
import ani.dantotsu.navBarHeight
|
|
||||||
import ani.dantotsu.media.novel.NovelReadFragment
|
import ani.dantotsu.media.novel.NovelReadFragment
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
@@ -47,6 +48,7 @@ import ani.dantotsu.saveData
|
|||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
@@ -71,6 +73,16 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||||
|
if (media.name == "No media found") {
|
||||||
|
snackString(media.name)
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mediaSingleton = null
|
||||||
|
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||||
|
MediaSingleton.bitmap = null
|
||||||
|
|
||||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||||
@@ -79,11 +91,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv)
|
|
||||||
|
|
||||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||||
|
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||||
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
||||||
|
|
||||||
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -101,17 +113,22 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
if (uiSettings.bannerAnimations) {
|
if (uiSettings.bannerAnimations) {
|
||||||
val adi = AccelerateDecelerateInterpolator()
|
val adi = AccelerateDecelerateInterpolator()
|
||||||
val generator = RandomTransitionGenerator((10000 + 15000 * (uiSettings.animationSpeed)).toLong(), adi)
|
val generator = RandomTransitionGenerator(
|
||||||
|
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||||
|
adi
|
||||||
|
)
|
||||||
binding.mediaBanner.setTransitionGenerator(generator)
|
binding.mediaBanner.setTransitionGenerator(generator)
|
||||||
}
|
}
|
||||||
val banner = if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
val banner =
|
||||||
|
if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||||
val viewPager = binding.mediaViewPager
|
val viewPager = binding.mediaViewPager
|
||||||
tabLayout = binding.mediaTab as NavigationBarView
|
tabLayout = binding.mediaTab as NavigationBarView
|
||||||
viewPager.isUserInputEnabled = false
|
viewPager.isUserInputEnabled = false
|
||||||
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||||
|
|
||||||
var media: Media = intent.getSerialized("media") ?: return
|
|
||||||
media.selected = model.loadSelected(media)
|
val isDownload = intent.getBooleanExtra("download", false)
|
||||||
|
media.selected = model.loadSelected(media, isDownload)
|
||||||
|
|
||||||
binding.mediaCoverImage.loadImage(media.cover)
|
binding.mediaCoverImage.loadImage(media.cover)
|
||||||
binding.mediaCoverImage.setOnLongClickListener {
|
binding.mediaCoverImage.setOnLongClickListener {
|
||||||
@@ -142,7 +159,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
||||||
|
if (this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("incognito", false)) {
|
||||||
|
binding.mediaTitle.text = " ${media.userPreferredName}"
|
||||||
|
binding.incognito.visibility = View.VISIBLE
|
||||||
|
}else {
|
||||||
binding.mediaTitle.text = media.userPreferredName
|
binding.mediaTitle.text = media.userPreferredName
|
||||||
|
}
|
||||||
binding.mediaTitle.setOnLongClickListener {
|
binding.mediaTitle.setOnLongClickListener {
|
||||||
copyToClipboard(media.userPreferredName)
|
copyToClipboard(media.userPreferredName)
|
||||||
true
|
true
|
||||||
@@ -162,13 +185,28 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
R.drawable.ic_round_favorite_24
|
R.drawable.ic_round_favorite_24
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
this.theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val color = typedValue.data
|
||||||
|
val typedValue2 = TypedValue()
|
||||||
|
this.theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
|
typedValue2,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val color2 = typedValue.data
|
||||||
|
|
||||||
PopImageButton(
|
PopImageButton(
|
||||||
scope,
|
scope,
|
||||||
binding.mediaFav,
|
binding.mediaFav,
|
||||||
R.drawable.ic_round_favorite_24,
|
R.drawable.ic_round_favorite_24,
|
||||||
R.drawable.ic_round_favorite_border_24,
|
R.drawable.ic_round_favorite_border_24,
|
||||||
R.color.nav_tab,
|
R.color.bg_opp,
|
||||||
R.color.fav,
|
R.color.violet_400,//TODO: Change to colorSecondary
|
||||||
media.isFav
|
media.isFav
|
||||||
) {
|
) {
|
||||||
media.isFav = it
|
media.isFav = it
|
||||||
@@ -180,17 +218,36 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ResourceType")
|
||||||
fun total() {
|
fun total() {
|
||||||
val text = SpannableStringBuilder().apply {
|
val text = SpannableStringBuilder().apply {
|
||||||
val white = ContextCompat.getColor(this@MediaDetailsActivity, R.color.bg_opp)
|
val typedValue = TypedValue()
|
||||||
|
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorOnBackground,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val white = typedValue.data
|
||||||
if (media.userStatus != null) {
|
if (media.userStatus != null) {
|
||||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue, true)
|
theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
bold { color(typedValue.data) { append("${media.userProgress}") } }
|
bold { color(typedValue.data) { append("${media.userProgress}") } }
|
||||||
append(if (media.anime != null) getString(R.string.episodes_out_of) else getString(R.string.chapters_out_of))
|
append(
|
||||||
|
if (media.anime != null) getString(R.string.episodes_out_of) else getString(
|
||||||
|
R.string.chapters_out_of
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
append(if (media.anime != null) getString(R.string.episodes_total_of) else getString(R.string.chapters_total_of))
|
append(
|
||||||
|
if (media.anime != null) getString(R.string.episodes_total_of) else getString(
|
||||||
|
R.string.chapters_total_of
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
if (media.anime!!.nextAiringEpisode != null) {
|
if (media.anime!!.nextAiringEpisode != null) {
|
||||||
@@ -206,8 +263,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
fun progress() {
|
fun progress() {
|
||||||
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
||||||
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
|
val statusStrings =
|
||||||
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
|
||||||
|
R.array.status_manga
|
||||||
|
)
|
||||||
|
val userStatus =
|
||||||
|
if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
||||||
|
|
||||||
if (media.userStatus != null) {
|
if (media.userStatus != null) {
|
||||||
binding.mediaTotal.visibility = View.VISIBLE
|
binding.mediaTotal.visibility = View.VISIBLE
|
||||||
@@ -255,14 +316,22 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
adult = media.isAdult
|
adult = media.isAdult
|
||||||
|
|
||||||
tabLayout.menu.clear()
|
tabLayout.menu.clear()
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
|
viewPager.adapter =
|
||||||
|
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
|
||||||
tabLayout.inflateMenu(R.menu.anime_menu_detail)
|
tabLayout.inflateMenu(R.menu.anime_menu_detail)
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, if(media.format=="NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA)
|
viewPager.adapter = ViewPagerAdapter(
|
||||||
|
supportFragmentManager,
|
||||||
|
lifecycle,
|
||||||
|
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
|
||||||
|
)
|
||||||
|
if (media.format == "NOVEL") {
|
||||||
|
tabLayout.inflateMenu(R.menu.novel_menu_detail)
|
||||||
|
} else {
|
||||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||||
|
}
|
||||||
anime = false
|
anime = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +343,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
tabLayout.setOnItemSelectedListener { item ->
|
tabLayout.setOnItemSelectedListener { item ->
|
||||||
selectFromID(item.itemId)
|
selectFromID(item.itemId)
|
||||||
viewPager.setCurrentItem(selected, false)
|
viewPager.setCurrentItem(selected, false)
|
||||||
val sel = model.loadSelected(media)
|
val sel = model.loadSelected(media, isDownload)
|
||||||
sel.window = selected
|
sel.window = selected
|
||||||
model.saveSelected(media.id, sel, this)
|
model.saveSelected(media.id, sel, this)
|
||||||
true
|
true
|
||||||
@@ -306,6 +375,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
R.id.info -> {
|
R.id.info -> {
|
||||||
selected = 0
|
selected = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.watch, R.id.read -> {
|
R.id.watch, R.id.read -> {
|
||||||
selected = 1
|
selected = 1
|
||||||
}
|
}
|
||||||
@@ -332,6 +402,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
private enum class SupportedMedia {
|
private enum class SupportedMedia {
|
||||||
ANIME, MANGA, NOVEL
|
ANIME, MANGA, NOVEL
|
||||||
}
|
}
|
||||||
|
|
||||||
//ViewPager
|
//ViewPager
|
||||||
private class ViewPagerAdapter(
|
private class ViewPagerAdapter(
|
||||||
fragmentManager: FragmentManager,
|
fragmentManager: FragmentManager,
|
||||||
@@ -349,6 +420,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
SupportedMedia.MANGA -> MangaReadFragment()
|
SupportedMedia.MANGA -> MangaReadFragment()
|
||||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> MediaInfoFragment()
|
else -> MediaInfoFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,27 +435,43 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
|
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
|
||||||
val percentage = abs(i) * 100 / mMaxScrollSize
|
val percentage = abs(i) * 100 / mMaxScrollSize
|
||||||
|
|
||||||
binding.mediaCover.visibility = if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
binding.mediaCover.visibility =
|
||||||
|
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||||
val duration = (200 * uiSettings.animationSpeed).toLong()
|
val duration = (200 * uiSettings.animationSpeed).toLong()
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val color = typedValue.data
|
||||||
if (percentage >= percent && !isCollapsed) {
|
if (percentage >= percent && !isCollapsed) {
|
||||||
isCollapsed = true
|
isCollapsed = true
|
||||||
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration).start()
|
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
|
||||||
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", screenWidth).setDuration(duration).start()
|
.start()
|
||||||
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", screenWidth).setDuration(duration).start()
|
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", screenWidth)
|
||||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth).setDuration(duration).start()
|
.setDuration(duration).start()
|
||||||
|
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", screenWidth)
|
||||||
|
.setDuration(duration).start()
|
||||||
|
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
|
||||||
|
.setDuration(duration).start()
|
||||||
binding.mediaBanner.pause()
|
binding.mediaBanner.pause()
|
||||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
|
||||||
}
|
}
|
||||||
if (percentage <= percent && isCollapsed) {
|
if (percentage <= percent && isCollapsed) {
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", -screenWidth).setDuration(duration).start()
|
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", -screenWidth)
|
||||||
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", 0f).setDuration(duration).start()
|
.setDuration(duration).start()
|
||||||
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", 0f).setDuration(duration).start()
|
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", 0f)
|
||||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f).setDuration(duration).start()
|
.setDuration(duration).start()
|
||||||
|
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", 0f).setDuration(duration)
|
||||||
|
.start()
|
||||||
|
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
|
||||||
|
.setDuration(duration).start()
|
||||||
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
||||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv)
|
|
||||||
}
|
}
|
||||||
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(false)
|
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
|
||||||
|
false
|
||||||
|
)
|
||||||
if (percentage == 0 && model.scrolledToTop.value != true) model.scrolledToTop.postValue(true)
|
if (percentage == 0 && model.scrolledToTop.value != true) model.scrolledToTop.postValue(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +492,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
init {
|
init {
|
||||||
enabled(true)
|
enabled(true)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
delay(100) //TODO: a listener would be better
|
||||||
clicked()
|
clicked()
|
||||||
}
|
}
|
||||||
image.setOnClickListener {
|
image.setOnClickListener {
|
||||||
@@ -425,8 +514,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
ObjectAnimator.ofFloat(image, "scaleX", 1f, 0f).setDuration(69).start()
|
ObjectAnimator.ofFloat(image, "scaleX", 1f, 0f).setDuration(69).start()
|
||||||
ObjectAnimator.ofFloat(image, "scaleY", 1f, 0f).setDuration(100).start()
|
ObjectAnimator.ofFloat(image, "scaleY", 1f, 0f).setDuration(100).start()
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
||||||
if (clicked) {
|
if (clicked) {
|
||||||
ObjectAnimator.ofArgb(image,
|
ObjectAnimator.ofArgb(
|
||||||
|
image,
|
||||||
"ColorFilter",
|
"ColorFilter",
|
||||||
ContextCompat.getColor(context, c1),
|
ContextCompat.getColor(context, c1),
|
||||||
ContextCompat.getColor(context, c2)
|
ContextCompat.getColor(context, c2)
|
||||||
@@ -439,18 +530,24 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
ObjectAnimator.ofFloat(image, "scaleX", 1.5f, 1f).setDuration(100).start()
|
ObjectAnimator.ofFloat(image, "scaleX", 1.5f, 1f).setDuration(100).start()
|
||||||
ObjectAnimator.ofFloat(image, "scaleY", 1.5f, 1f).setDuration(100).start()
|
ObjectAnimator.ofFloat(image, "scaleY", 1.5f, 1f).setDuration(100).start()
|
||||||
delay(200)
|
delay(200)
|
||||||
if (clicked) ObjectAnimator.ofArgb(
|
if (clicked) {
|
||||||
|
ObjectAnimator.ofArgb(
|
||||||
image,
|
image,
|
||||||
"ColorFilter",
|
"ColorFilter",
|
||||||
ContextCompat.getColor(context, c2),
|
ContextCompat.getColor(context, c2),
|
||||||
ContextCompat.getColor(context, c1)
|
ContextCompat.getColor(context, c1)
|
||||||
).setDuration(200).start()
|
).setDuration(200).start()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun enabled(enabled: Boolean) {
|
fun enabled(enabled: Boolean) {
|
||||||
disabled = !enabled
|
disabled = !enabled
|
||||||
image.alpha = if (disabled) 0.33f else 1f
|
image.alpha = if (disabled) 0.33f else 1f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var mediaSingleton: Media? = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -9,18 +8,22 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.media.anime.Episode
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.logger
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.media.anime.Episode
|
||||||
|
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||||
import ani.dantotsu.media.manga.MangaChapter
|
import ani.dantotsu.media.manga.MangaChapter
|
||||||
import ani.dantotsu.others.AniSkip
|
import ani.dantotsu.others.AniSkip
|
||||||
import ani.dantotsu.others.Jikan
|
import ani.dantotsu.others.Jikan
|
||||||
import ani.dantotsu.others.Kitsu
|
import ani.dantotsu.others.Kitsu
|
||||||
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.Book
|
import ani.dantotsu.parsers.Book
|
||||||
import ani.dantotsu.parsers.MangaImage
|
import ani.dantotsu.parsers.MangaImage
|
||||||
import ani.dantotsu.parsers.MangaReadSources
|
import ani.dantotsu.parsers.MangaReadSources
|
||||||
|
import ani.dantotsu.parsers.MangaSources
|
||||||
import ani.dantotsu.parsers.NovelSources
|
import ani.dantotsu.parsers.NovelSources
|
||||||
import ani.dantotsu.parsers.ShowResponse
|
import ani.dantotsu.parsers.ShowResponse
|
||||||
import ani.dantotsu.parsers.VideoExtractor
|
import ani.dantotsu.parsers.VideoExtractor
|
||||||
@@ -28,20 +31,10 @@ import ani.dantotsu.parsers.WatchSources
|
|||||||
import ani.dantotsu.saveData
|
import ani.dantotsu.saveData
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import ani.dantotsu.currContext
|
|
||||||
import ani.dantotsu.R
|
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
|
||||||
import ani.dantotsu.parsers.AniyomiAdapter
|
|
||||||
import ani.dantotsu.parsers.DynamicMangaParser
|
|
||||||
import ani.dantotsu.parsers.HAnimeSources
|
|
||||||
import ani.dantotsu.parsers.HMangaSources
|
|
||||||
import ani.dantotsu.parsers.MangaSources
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@@ -53,7 +46,7 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun loadSelected(media: Media): Selected {
|
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
|
||||||
val sharedPreferences = Injekt.get<SharedPreferences>()
|
val sharedPreferences = Injekt.get<SharedPreferences>()
|
||||||
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
|
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
|
||||||
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
|
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
|
||||||
@@ -64,13 +57,24 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
saveSelected(media.id, it)
|
saveSelected(media.id, it)
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
|
if (isDownload) {
|
||||||
|
data.sourceIndex = if (media.anime != null) {
|
||||||
|
AnimeSources.list.size - 1
|
||||||
|
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
||||||
|
MangaSources.list.size - 1
|
||||||
|
} else {
|
||||||
|
NovelSources.list.size - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadSelectedStringLocation(sourceName: String): Int {
|
fun loadSelectedStringLocation(sourceName: String): Int {
|
||||||
//find the location of the source in the list
|
//find the location of the source in the list
|
||||||
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
||||||
if (location == -1) {location = 0}
|
if (location == -1) {
|
||||||
|
location = 0
|
||||||
|
}
|
||||||
return location
|
return location
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +99,9 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
|
|
||||||
|
|
||||||
//Anime
|
//Anime
|
||||||
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null)
|
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> =
|
||||||
|
MutableLiveData<Map<String, Episode>>(null)
|
||||||
|
|
||||||
fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes
|
fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes
|
||||||
suspend fun loadKitsuEpisodes(s: Media) {
|
suspend fun loadKitsuEpisodes(s: Media) {
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
@@ -103,7 +109,9 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null)
|
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> =
|
||||||
|
MutableLiveData<Map<String, Episode>>(null)
|
||||||
|
|
||||||
fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes
|
fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes
|
||||||
suspend fun loadFillerEpisodes(s: Media) {
|
suspend fun loadFillerEpisodes(s: Media) {
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
@@ -120,8 +128,8 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
|
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
|
||||||
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
|
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
|
||||||
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
|
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
|
||||||
suspend fun loadEpisodes(media: Media, i: Int) {
|
suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) {
|
||||||
if (!epsLoaded.containsKey(i)) {
|
if (!epsLoaded.containsKey(i) || invalidate) {
|
||||||
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
|
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
|
||||||
}
|
}
|
||||||
episodes.postValue(epsLoaded)
|
episodes.postValue(epsLoaded)
|
||||||
@@ -134,7 +142,8 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
|
|
||||||
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
|
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
|
||||||
watchSources?.saveResponse(i, id, source)
|
watchSources?.saveResponse(i, id, source)
|
||||||
epsLoaded[i] = watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
|
epsLoaded[i] =
|
||||||
|
watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
|
||||||
episodes.postValue(epsLoaded)
|
episodes.postValue(epsLoaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +182,12 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
|
|
||||||
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
|
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
|
||||||
private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf()
|
private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf()
|
||||||
suspend fun loadTimeStamps(malId: Int?, episodeNum: Int?, duration: Long, useProxyForTimeStamps: Boolean) {
|
suspend fun loadTimeStamps(
|
||||||
|
malId: Int?,
|
||||||
|
episodeNum: Int?,
|
||||||
|
duration: Long,
|
||||||
|
useProxyForTimeStamps: Boolean
|
||||||
|
) {
|
||||||
malId ?: return
|
malId ?: return
|
||||||
episodeNum ?: return
|
episodeNum ?: return
|
||||||
if (timeStampsMap.containsKey(episodeNum))
|
if (timeStampsMap.containsKey(episodeNum))
|
||||||
@@ -183,7 +197,11 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
timeStamps.postValue(result)
|
timeStamps.postValue(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadEpisodeSingleVideo(ep: Episode, selected: Selected, post: Boolean = true): Boolean {
|
suspend fun loadEpisodeSingleVideo(
|
||||||
|
ep: Episode,
|
||||||
|
selected: Selected,
|
||||||
|
post: Boolean = true
|
||||||
|
): Boolean {
|
||||||
if (ep.extractors.isNullOrEmpty()) {
|
if (ep.extractors.isNullOrEmpty()) {
|
||||||
|
|
||||||
val server = selected.server ?: return false
|
val server = selected.server ?: return false
|
||||||
@@ -193,8 +211,10 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
selected.sourceIndex = selected.sourceIndex
|
selected.sourceIndex = selected.sourceIndex
|
||||||
if (!post && !it.allowsPreloading) null
|
if (!post && !it.allowsPreloading) null
|
||||||
else ep.sEpisode?.let { it1 ->
|
else ep.sEpisode?.let { it1 ->
|
||||||
it.loadSingleVideoServer(server, link, ep.extra,
|
it.loadSingleVideoServer(
|
||||||
it1, post)
|
server, link, ep.extra,
|
||||||
|
it1, post
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} ?: return false)
|
} ?: return false)
|
||||||
ep.allStreams = false
|
ep.allStreams = false
|
||||||
@@ -217,7 +237,14 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val epChanged = MutableLiveData(true)
|
val epChanged = MutableLiveData(true)
|
||||||
fun onEpisodeClick(media: Media, i: String, manager: FragmentManager, launch: Boolean = true, prevEp: String? = null) {
|
fun onEpisodeClick(
|
||||||
|
media: Media,
|
||||||
|
i: String,
|
||||||
|
manager: FragmentManager,
|
||||||
|
launch: Boolean = true,
|
||||||
|
prevEp: String? = null,
|
||||||
|
isDownload: Boolean = false
|
||||||
|
) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
|
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
|
||||||
if (media.anime?.episodes?.get(i) != null) {
|
if (media.anime?.episodes?.get(i) != null) {
|
||||||
@@ -227,23 +254,32 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
media.selected = this.loadSelected(media)
|
media.selected = this.loadSelected(media)
|
||||||
val selector = SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
|
val selector =
|
||||||
|
SelectorDialogFragment.newInstance(
|
||||||
|
media.selected!!.server,
|
||||||
|
launch,
|
||||||
|
prevEp,
|
||||||
|
isDownload
|
||||||
|
)
|
||||||
selector.show(manager, "dialog")
|
selector.show(manager, "dialog")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Manga
|
//Manga
|
||||||
var mangaReadSources: MangaReadSources? = null
|
var mangaReadSources: MangaReadSources? = null
|
||||||
|
|
||||||
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
|
private val mangaChapters =
|
||||||
|
MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
|
||||||
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
|
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
|
||||||
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters
|
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> =
|
||||||
suspend fun loadMangaChapters(media: Media, i: Int) {
|
mangaChapters
|
||||||
|
|
||||||
|
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
||||||
logger("Loading Manga Chapters : $mangaLoaded")
|
logger("Loading Manga Chapters : $mangaLoaded")
|
||||||
if (!mangaLoaded.containsKey(i)) tryWithSuspend {
|
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
||||||
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
mangaLoaded[i] =
|
||||||
|
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||||
}
|
}
|
||||||
mangaChapters.postValue(mangaLoaded)
|
mangaChapters.postValue(mangaLoaded)
|
||||||
}
|
}
|
||||||
@@ -258,10 +294,17 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
|
|
||||||
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
||||||
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
||||||
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
|
suspend fun loadMangaChapterImages(
|
||||||
|
chapter: MangaChapter,
|
||||||
|
selected: Selected,
|
||||||
|
series: String,
|
||||||
|
post: Boolean = true
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
return tryWithSuspend(true) {
|
return tryWithSuspend(true) {
|
||||||
chapter.addImages(
|
chapter.addImages(
|
||||||
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
|
mangaReadSources?.get(selected.sourceIndex)
|
||||||
|
?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
|
||||||
)
|
)
|
||||||
if (post) mangaChapter.postValue(chapter)
|
if (post) mangaChapter.postValue(chapter)
|
||||||
true
|
true
|
||||||
@@ -269,13 +312,15 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? {
|
fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? {
|
||||||
return if (mangaImage.useTransformation) mangaReadSources?.get(source)?.getTransformation() else null
|
return if (mangaImage.useTransformation) mangaReadSources?.get(source)
|
||||||
|
?.getTransformation() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
val novelSources = NovelSources
|
val novelSources = NovelSources
|
||||||
val novelResponses = MutableLiveData<List<ShowResponse>>(null)
|
val novelResponses = MutableLiveData<List<ShowResponse>>(null)
|
||||||
suspend fun searchNovels(query: String, i: Int) {
|
suspend fun searchNovels(query: String, i: Int) {
|
||||||
val source = novelSources[i]
|
val position = if (i >= novelSources.list.size) 0 else i
|
||||||
|
val source = novelSources[position]
|
||||||
tryWithSuspend(post = true) {
|
tryWithSuspend(post = true) {
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
novelResponses.postValue(source.search(query))
|
novelResponses.postValue(source.search(query))
|
||||||
@@ -295,7 +340,9 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
val book: MutableLiveData<Book> = MutableLiveData(null)
|
val book: MutableLiveData<Book> = MutableLiveData(null)
|
||||||
suspend fun loadBook(novel: ShowResponse, i: Int) {
|
suspend fun loadBook(novel: ShowResponse, i: Int) {
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
book.postValue(novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend)
|
book.postValue(
|
||||||
|
novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ani.dantotsu.media
|
|||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -43,7 +44,11 @@ class MediaInfoFragment : Fragment() {
|
|||||||
private var type = "ANIME"
|
private var type = "ANIME"
|
||||||
private val genreModel: GenresViewModel by activityViewModels()
|
private val genreModel: GenresViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
|
_binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -55,6 +60,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val model: MediaDetailsViewModel by activityViewModels()
|
val model: MediaDetailsViewModel by activityViewModels()
|
||||||
|
val offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("offlineMode", false) || !isOnline(requireContext())
|
||||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||||
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
||||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||||
@@ -73,13 +79,15 @@ class MediaInfoFragment : Fragment() {
|
|||||||
copyToClipboard(media.name ?: media.nameRomaji)
|
copyToClipboard(media.name ?: media.nameRomaji)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = View.VISIBLE
|
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
|
||||||
|
View.VISIBLE
|
||||||
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
||||||
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
||||||
copyToClipboard(media.nameRomaji)
|
copyToClipboard(media.nameRomaji)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
binding.mediaInfoMeanScore.text = if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
|
binding.mediaInfoMeanScore.text =
|
||||||
|
if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
|
||||||
binding.mediaInfoStatus.text = media.status
|
binding.mediaInfoStatus.text = media.status
|
||||||
binding.mediaInfoFormat.text = media.format
|
binding.mediaInfoFormat.text = media.format
|
||||||
binding.mediaInfoSource.text = media.source
|
binding.mediaInfoSource.text = media.source
|
||||||
@@ -95,6 +103,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
if (media.anime.mainStudio != null) {
|
if (media.anime.mainStudio != null) {
|
||||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||||
|
if (!offline) {
|
||||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
@@ -106,9 +115,11 @@ class MediaInfoFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (media.anime.author != null) {
|
if (media.anime.author != null) {
|
||||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoAuthor.text = media.anime.author!!.name
|
binding.mediaInfoAuthor.text = media.anime.author!!.name
|
||||||
|
if (!offline) {
|
||||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
@@ -120,6 +131,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||||
binding.mediaInfoTotal.text =
|
binding.mediaInfoTotal.text =
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
|
||||||
@@ -131,6 +143,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
if (media.manga.author != null) {
|
if (media.manga.author != null) {
|
||||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoAuthor.text = media.manga.author!!.name
|
binding.mediaInfoAuthor.text = media.manga.author!!.name
|
||||||
|
if (!offline) {
|
||||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
@@ -143,6 +156,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val desc = HtmlCompat.fromHtml(
|
val desc = HtmlCompat.fromHtml(
|
||||||
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
|
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
|
||||||
@@ -183,7 +197,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.trailer != null) {
|
if (media.trailer != null && !offline) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
class MyChrome : WebChromeClient() {
|
class MyChrome : WebChromeClient() {
|
||||||
private var mCustomView: View? = null
|
private var mCustomView: View? = null
|
||||||
@@ -237,7 +251,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) {
|
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty()) && !offline) {
|
||||||
val markWon = Markwon.builder(requireContext())
|
val markWon = Markwon.builder(requireContext())
|
||||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||||
|
|
||||||
@@ -246,7 +260,12 @@ class MediaInfoFragment : Fragment() {
|
|||||||
val end = a.indexOf('"', first).let { if (it != -1) it else return a }
|
val end = a.indexOf('"', first).let { if (it != -1) it else return a }
|
||||||
val name = a.subSequence(first, end).toString()
|
val name = a.subSequence(first, end).toString()
|
||||||
return "${a.subSequence(0, first)}" +
|
return "${a.subSequence(0, first)}" +
|
||||||
"[$name](https://www.youtube.com/results?search_query=${URLEncoder.encode(name, "utf-8")})" +
|
"[$name](https://www.youtube.com/results?search_query=${
|
||||||
|
URLEncoder.encode(
|
||||||
|
name,
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
})" +
|
||||||
"${a.subSequence(end, a.length)}"
|
"${a.subSequence(end, a.length)}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +289,11 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (media.anime.op.isNotEmpty()) {
|
if (media.anime.op.isNotEmpty()) {
|
||||||
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false)
|
val bind = ItemTitleTextBinding.inflate(
|
||||||
|
LayoutInflater.from(context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
bind.itemTitle.setText(R.string.opening)
|
bind.itemTitle.setText(R.string.opening)
|
||||||
makeText(bind.itemText, media.anime.op)
|
makeText(bind.itemText, media.anime.op)
|
||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
@@ -278,14 +301,18 @@ class MediaInfoFragment : Fragment() {
|
|||||||
|
|
||||||
|
|
||||||
if (media.anime.ed.isNotEmpty()) {
|
if (media.anime.ed.isNotEmpty()) {
|
||||||
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false)
|
val bind = ItemTitleTextBinding.inflate(
|
||||||
|
LayoutInflater.from(context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
bind.itemTitle.setText(R.string.ending)
|
bind.itemTitle.setText(R.string.ending)
|
||||||
makeText(bind.itemText, media.anime.ed)
|
makeText(bind.itemText, media.anime.ed)
|
||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.genres.isNotEmpty()) {
|
if (media.genres.isNotEmpty() && !offline) {
|
||||||
val bind = ActivityGenreBinding.inflate(
|
val bind = ActivityGenreBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
@@ -316,7 +343,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.tags.isNotEmpty()) {
|
if (media.tags.isNotEmpty() && !offline) {
|
||||||
val bind = ItemTitleChipgroupBinding.inflate(
|
val bind = ItemTitleChipgroupBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
@@ -357,7 +384,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!media.characters.isNullOrEmpty()) {
|
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||||
val bind = ItemTitleRecyclerBinding.inflate(
|
val bind = ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
@@ -374,7 +401,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!media.relations.isNullOrEmpty()) {
|
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||||
if (media.sequel != null || media.prequel != null) {
|
if (media.sequel != null || media.prequel != null) {
|
||||||
val bind = ItemQuelsBinding.inflate(
|
val bind = ItemQuelsBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
@@ -437,7 +464,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bindi.root)
|
parent.addView(bindi.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!media.recommendations.isNullOrEmpty()) {
|
if (!media.recommendations.isNullOrEmpty() && !offline ) {
|
||||||
val bind = ItemTitleRecyclerBinding.inflate(
|
val bind = ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
@@ -458,7 +485,8 @@ class MediaInfoFragment : Fragment() {
|
|||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200)
|
val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200)
|
||||||
val cornerNotTop = ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
|
val cornerNotTop =
|
||||||
|
ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
|
||||||
var cornered = true
|
var cornered = true
|
||||||
cornerTop.start()
|
cornerTop.start()
|
||||||
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->
|
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||||
|
import com.google.android.material.materialswitch.MaterialSwitch
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -27,7 +27,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
private var _binding: BottomSheetMediaListBinding? = null
|
private var _binding: BottomSheetMediaListBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetMediaListBinding.inflate(inflater, container, false)
|
_binding = BottomSheetMediaListBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -46,8 +50,12 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
binding.mediaListLayout.visibility = View.VISIBLE
|
binding.mediaListLayout.visibility = View.VISIBLE
|
||||||
|
|
||||||
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
||||||
val statusStrings = if (media?.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
|
val statusStrings =
|
||||||
val userStatus = if(media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
|
if (media?.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
|
||||||
|
R.array.status_manga
|
||||||
|
)
|
||||||
|
val userStatus =
|
||||||
|
if (media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
|
||||||
|
|
||||||
binding.mediaListStatus.setText(userStatus)
|
binding.mediaListStatus.setText(userStatus)
|
||||||
binding.mediaListStatus.setAdapter(
|
binding.mediaListStatus.setAdapter(
|
||||||
@@ -160,7 +168,9 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
val init =
|
val init =
|
||||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||||
.toInt() else 0
|
.toInt() else 0
|
||||||
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
|
if (init < (total
|
||||||
|
?: 5000)
|
||||||
|
) binding.mediaListProgress.setText((init + 1).toString())
|
||||||
if (init + 1 == (total ?: 5000)) {
|
if (init + 1 == (total ?: 5000)) {
|
||||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||||
onComplete()
|
onComplete()
|
||||||
@@ -186,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
media?.inCustomListsOf?.forEach {
|
media?.inCustomListsOf?.forEach {
|
||||||
SwitchMaterial(requireContext()).apply {
|
MaterialSwitch(requireContext()).apply {
|
||||||
isChecked = it.value
|
isChecked = it.value
|
||||||
text = it.key
|
text = it.key
|
||||||
setOnCheckedChangeListener { _, isChecked ->
|
setOnCheckedChangeListener { _, isChecked ->
|
||||||
@@ -201,11 +211,15 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
val progress =
|
||||||
|
_binding?.mediaListProgress?.text.toString().toIntOrNull()
|
||||||
val score =
|
val score =
|
||||||
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
|
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()
|
||||||
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
?.times(10))?.toInt()
|
||||||
val rewatch = _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
|
val status =
|
||||||
|
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||||
|
val rewatch =
|
||||||
|
_binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
|
||||||
val notes = _binding?.mediaListNotes?.text?.toString()
|
val notes = _binding?.mediaListNotes?.text?.toString()
|
||||||
val startD = start.date
|
val startD = start.date
|
||||||
val endD = end.date
|
val endD = end.date
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import androidx.core.view.updateLayoutParams
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
|
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -44,7 +44,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
private var _binding: BottomSheetMediaListSmallBinding? = null
|
private var _binding: BottomSheetMediaListSmallBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false)
|
_binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -58,8 +62,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
binding.mediaListProgressBar.visibility = View.GONE
|
binding.mediaListProgressBar.visibility = View.GONE
|
||||||
binding.mediaListLayout.visibility = View.VISIBLE
|
binding.mediaListLayout.visibility = View.VISIBLE
|
||||||
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
||||||
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
|
val statusStrings =
|
||||||
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
|
||||||
|
R.array.status_manga
|
||||||
|
)
|
||||||
|
val userStatus =
|
||||||
|
if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
||||||
|
|
||||||
binding.mediaListStatus.setText(userStatus)
|
binding.mediaListStatus.setText(userStatus)
|
||||||
binding.mediaListStatus.setAdapter(
|
binding.mediaListStatus.setAdapter(
|
||||||
@@ -128,10 +136,26 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
||||||
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
|
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()
|
||||||
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
?.times(10))?.toInt()
|
||||||
Anilist.mutation.editList(media.id, progress, score, null, null, status, media.isListPrivate)
|
val status =
|
||||||
MAL.query.editList(media.idMAL, media.anime != null, progress, score, status)
|
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||||
|
Anilist.mutation.editList(
|
||||||
|
media.id,
|
||||||
|
progress,
|
||||||
|
score,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
status,
|
||||||
|
media.isListPrivate
|
||||||
|
)
|
||||||
|
MAL.query.editList(
|
||||||
|
media.idMAL,
|
||||||
|
media.anime != null,
|
||||||
|
progress,
|
||||||
|
score,
|
||||||
|
status
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Refresh.all()
|
Refresh.all()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
class OtherDetailsViewModel : ViewModel() {
|
class OtherDetailsViewModel : ViewModel() {
|
||||||
private val character: MutableLiveData<Character> = MutableLiveData(null)
|
private val character: MutableLiveData<Character> = MutableLiveData(null)
|
||||||
@@ -19,11 +19,13 @@ class OtherDetailsViewModel : ViewModel() {
|
|||||||
suspend fun loadStudio(m: Studio) {
|
suspend fun loadStudio(m: Studio) {
|
||||||
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
|
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val author: MutableLiveData<Author> = MutableLiveData(null)
|
private val author: MutableLiveData<Author> = MutableLiveData(null)
|
||||||
fun getAuthor(): LiveData<Author> = author
|
fun getAuthor(): LiveData<Author> = author
|
||||||
suspend fun loadAuthor(m: Author) {
|
suspend fun loadAuthor(m: Author) {
|
||||||
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
|
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
|
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
|
||||||
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
||||||
suspend fun loadCalendar() {
|
suspend fun loadCalendar() {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
|||||||
var bar: ProgressBar? = null
|
var bar: ProgressBar? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
|
||||||
val binding = ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return ProgressViewHolder(binding)
|
return ProgressViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +34,12 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
|||||||
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
|
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
|
||||||
override fun onDoubleClick(event: MotionEvent) {
|
override fun onDoubleClick(event: MotionEvent) {
|
||||||
snackString(currContext()?.getString(R.string.cant_wait))
|
snackString(currContext()?.getString(R.string.cant_wait))
|
||||||
ObjectAnimator.ofFloat(progressBar, "translationX", progressBar.translationX, progressBar.translationX + 100f)
|
ObjectAnimator.ofFloat(
|
||||||
|
progressBar,
|
||||||
|
"translationX",
|
||||||
|
progressBar.translationX,
|
||||||
|
progressBar.translationX + 100f
|
||||||
|
)
|
||||||
.setDuration(300).start()
|
.setDuration(300).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +57,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
|
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||||
import ani.dantotsu.connections.anilist.SearchResults
|
import ani.dantotsu.connections.anilist.SearchResults
|
||||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -37,6 +39,8 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivitySearchBinding.inflate(layoutInflater)
|
binding = ActivitySearchBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
@@ -74,7 +78,7 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
|
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
|
||||||
val headerAdaptor = SearchAdapter(this)
|
val headerAdaptor = SearchAdapter(this)
|
||||||
|
|
||||||
val gridSize = (screenWidth / 124f).toInt()
|
val gridSize = (screenWidth / 120f).toInt()
|
||||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
override fun getSpanSize(position: Int): Int {
|
override fun getSpanSize(position: Int): Int {
|
||||||
@@ -155,7 +159,7 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
fun search() {
|
fun search() {
|
||||||
val size = model.searchResults.results.size
|
val size = model.searchResults.results.size
|
||||||
model.searchResults.results.clear()
|
model.searchResults.results.clear()
|
||||||
runOnUiThread {
|
binding.searchRecyclerView.post {
|
||||||
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -10,24 +12,30 @@ import android.view.ViewGroup
|
|||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||||
|
import ani.dantotsu.App.Companion.context
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
||||||
import ani.dantotsu.saveData
|
import ani.dantotsu.saveData
|
||||||
import com.google.android.material.checkbox.MaterialCheckBox.*
|
import com.google.android.material.checkbox.MaterialCheckBox.*
|
||||||
|
|
||||||
|
|
||||||
class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
class SearchAdapter(private val activity: SearchActivity) :
|
||||||
|
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
||||||
private val itemViewType = 6969
|
private val itemViewType = 6969
|
||||||
var search: Runnable? = null
|
var search: Runnable? = null
|
||||||
var requestFocus: Runnable? = null
|
var requestFocus: Runnable? = null
|
||||||
private var textWatcher: TextWatcher? = null
|
private var textWatcher: TextWatcher? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||||
val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return SearchHeaderViewHolder(binding)
|
return SearchHeaderViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,13 +44,15 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
|
|
||||||
|
|
||||||
val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
val imm: InputMethodManager =
|
||||||
|
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
|
||||||
when (activity.style) {
|
when (activity.style) {
|
||||||
0 -> {
|
0 -> {
|
||||||
binding.searchResultGrid.alpha = 1f
|
binding.searchResultGrid.alpha = 1f
|
||||||
binding.searchResultList.alpha = 0.33f
|
binding.searchResultList.alpha = 0.33f
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
binding.searchResultList.alpha = 1f
|
binding.searchResultList.alpha = 1f
|
||||||
binding.searchResultGrid.alpha = 0.33f
|
binding.searchResultGrid.alpha = 0.33f
|
||||||
@@ -50,6 +60,14 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.searchBar.hint = activity.result.type
|
binding.searchBar.hint = activity.result.type
|
||||||
|
if (currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getBoolean("incognito", false ) == true){
|
||||||
|
val startIconDrawableRes = R.drawable.ic_incognito_24
|
||||||
|
val startIconDrawable: Drawable? =
|
||||||
|
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
|
||||||
|
binding.searchBar.startIconDrawable = startIconDrawable
|
||||||
|
}
|
||||||
|
|
||||||
var adult = activity.result.isAdult
|
var adult = activity.result.isAdult
|
||||||
var listOnly = activity.result.onList
|
var listOnly = activity.result.onList
|
||||||
|
|
||||||
@@ -62,7 +80,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
||||||
activity.updateChips = { it.update() }
|
activity.updateChips = { it.update() }
|
||||||
}
|
}
|
||||||
binding.searchChipRecycler.layoutManager = LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
binding.searchChipRecycler.layoutManager =
|
||||||
|
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||||
|
|
||||||
binding.searchFilter.setOnClickListener {
|
binding.searchFilter.setOnClickListener {
|
||||||
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
||||||
@@ -70,7 +89,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
|
|
||||||
fun searchTitle() {
|
fun searchTitle() {
|
||||||
activity.result.apply {
|
activity.result.apply {
|
||||||
search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
|
search =
|
||||||
|
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
|
||||||
onList = listOnly
|
onList = listOnly
|
||||||
isAdult = adult
|
isAdult = adult
|
||||||
}
|
}
|
||||||
@@ -96,6 +116,7 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,20 +179,24 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return itemViewType
|
return itemViewType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchChipAdapter(val activity: SearchActivity) : RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
class SearchChipAdapter(val activity: SearchActivity) :
|
||||||
|
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||||
private var chips = activity.result.toChipList()
|
private var chips = activity.result.toChipList()
|
||||||
|
|
||||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
||||||
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return SearchChipViewHolder(binding)
|
return SearchChipViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,17 @@ import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
|||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
|
||||||
class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||||
private var _binding: BottomSheetSearchFilterBinding? = null
|
private var _binding: BottomSheetSearchFilterBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var activity: SearchActivity
|
private lateinit var activity: SearchActivity
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false)
|
_binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -99,7 +103,7 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
|||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
binding.root.context,
|
binding.root.context,
|
||||||
R.layout.item_dropdown,
|
R.layout.item_dropdown,
|
||||||
(1970 until 2024).map { it.toString() }.reversed().toTypedArray()
|
(1970 until 2025).map { it.toString() }.reversed().toTypedArray()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -129,7 +133,8 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
binding.searchGenresGrid.isChecked = false
|
binding.searchGenresGrid.isChecked = false
|
||||||
|
|
||||||
binding.searchFilterTags.adapter = FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
binding.searchFilterTags.adapter =
|
||||||
|
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
||||||
val tag = chip.text.toString()
|
val tag = chip.text.toString()
|
||||||
chip.isChecked = selectedTags.contains(tag)
|
chip.isChecked = selectedTags.contains(tag)
|
||||||
chip.isCloseIconVisible = exTags.contains(tag)
|
chip.isCloseIconVisible = exTags.contains(tag)
|
||||||
@@ -158,10 +163,12 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) :
|
class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) :
|
||||||
RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() {
|
RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() {
|
||||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
||||||
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return SearchChipViewHolder(binding)
|
return SearchChipViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ data class Selected(
|
|||||||
var chip: Int = 0,
|
var chip: Int = 0,
|
||||||
//var source: String = "",
|
//var source: String = "",
|
||||||
var sourceIndex: Int = 0,
|
var sourceIndex: Int = 0,
|
||||||
|
var langIndex: Int = 0,
|
||||||
var preferDub: Boolean = false,
|
var preferDub: Boolean = false,
|
||||||
var server: String? = null,
|
var server: String? = null,
|
||||||
var video: Int = 0,
|
var video: Int = 0,
|
||||||
var latest: Float = 0f,
|
var latest: Float = 0f,
|
||||||
|
var scanlators: List<String>? = null,
|
||||||
) : Serializable
|
) : Serializable
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ abstract class SourceAdapter(
|
|||||||
private val scope: CoroutineScope
|
private val scope: CoroutineScope
|
||||||
) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() {
|
) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder {
|
||||||
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return SourceViewHolder(binding)
|
return SourceViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ abstract class SourceAdapter(
|
|||||||
|
|
||||||
abstract suspend fun onItemClick(source: ShowResponse)
|
abstract suspend fun onItemClick(source: ShowResponse)
|
||||||
|
|
||||||
inner class SourceViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class SourceViewHolder(val binding: ItemCharacterBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
dialogFragment.dismiss()
|
dialogFragment.dismiss()
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.BottomSheetDialogFragment
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
import ani.dantotsu.media.anime.AnimeSourceAdapter
|
|
||||||
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
|
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
|
||||||
|
import ani.dantotsu.media.anime.AnimeSourceAdapter
|
||||||
import ani.dantotsu.media.manga.MangaSourceAdapter
|
import ani.dantotsu.media.manga.MangaSourceAdapter
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
@@ -38,7 +38,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
var id: Int? = null
|
var id: Int? = null
|
||||||
var media: Media? = null
|
var media: Media? = null
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false)
|
_binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -47,7 +51,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||||
|
|
||||||
val scope = requireActivity().lifecycleScope
|
val scope = requireActivity().lifecycleScope
|
||||||
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val imm =
|
||||||
|
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
model.getMedia().observe(viewLifecycleOwner) {
|
model.getMedia().observe(viewLifecycleOwner) {
|
||||||
media = it
|
media = it
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
@@ -65,6 +70,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
anime = false
|
anime = false
|
||||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search() {
|
fun search() {
|
||||||
binding.searchBarText.clearFocus()
|
binding.searchBarText.clearFocus()
|
||||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||||
@@ -86,6 +92,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
search()
|
search()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +108,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
|
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
|
||||||
binding.searchRecyclerView.layoutManager = GridLayoutManager(
|
binding.searchRecyclerView.layoutManager = GridLayoutManager(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
clamp(requireActivity().resources.displayMetrics.widthPixels / 124f.px, 1, 4)
|
clamp(
|
||||||
|
requireActivity().resources.displayMetrics.widthPixels / 124f.px,
|
||||||
|
1,
|
||||||
|
4
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,17 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.EmptyAdapter
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityStudioBinding
|
import ani.dantotsu.databinding.ActivityStudioBinding
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.px
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -28,6 +36,8 @@ class StudioActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityStudioBinding.inflate(layoutInflater)
|
binding = ActivityStudioBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
83
app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
Normal file
83
app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Request
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class SubtitleDownloader {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
//doesn't really download the subtitles -\_(o_o)_/-
|
||||||
|
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||||
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = networkHelper.client.newCall(request).execute()
|
||||||
|
|
||||||
|
// Check if response is successful
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val responseBody = response.body.string()
|
||||||
|
|
||||||
|
|
||||||
|
val subtitleType = when {
|
||||||
|
responseBody.contains("[Script Info]") -> SubtitleType.ASS
|
||||||
|
responseBody.contains("WEBVTT") -> SubtitleType.VTT
|
||||||
|
else -> SubtitleType.SRT
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext subtitleType
|
||||||
|
} else {
|
||||||
|
return@withContext SubtitleType.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//actually downloads lol
|
||||||
|
suspend fun downloadSubtitle(context: Context, url: String, downloadedType: DownloadedType) {
|
||||||
|
try {
|
||||||
|
val directory = DownloadsManager.getDirectory(context, downloadedType.type, downloadedType.title, downloadedType.chapter)
|
||||||
|
if (!directory.exists()) { //just in case
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
val type = loadSubtitleType(context, url)
|
||||||
|
val subtiteFile = File(directory, "subtitle.${type}")
|
||||||
|
if (subtiteFile.exists()) {
|
||||||
|
subtiteFile.delete()
|
||||||
|
}
|
||||||
|
subtiteFile.createNewFile()
|
||||||
|
|
||||||
|
val client = Injekt.get<NetworkHelper>().client
|
||||||
|
val request = Request.Builder().url(url).build()
|
||||||
|
val reponse = client.newCall(request).execute()
|
||||||
|
|
||||||
|
if (!reponse.isSuccessful) {
|
||||||
|
snackString("Failed to download subtitle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reponse.body.byteStream().use { input ->
|
||||||
|
subtiteFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snackString("Failed to download subtitle")
|
||||||
|
e.printStackTrace()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.databinding.ItemTitleBinding
|
import ani.dantotsu.databinding.ItemTitleBinding
|
||||||
|
|
||||||
class TitleAdapter(private val text: String) : RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
|
class TitleAdapter(private val text: String) :
|
||||||
inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root)
|
RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
|
||||||
|
inner class TitleViewHolder(val binding: ItemTitleBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
|
||||||
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
|
import java.util.regex.Matcher
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class AnimeNameAdapter {
|
||||||
|
companion object {
|
||||||
|
const val episodeRegex =
|
||||||
|
"(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||||
|
const val failedEpisodeNumberRegex =
|
||||||
|
"(?<!part\\s)\\b(\\d+)\\b"
|
||||||
|
const val seasonRegex = "\\s+(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||||
|
|
||||||
|
fun findSeasonNumber(text: String): Int? {
|
||||||
|
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||||
|
|
||||||
|
return if (seasonMatcher.find()) {
|
||||||
|
seasonMatcher.group(2)?.toInt()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findEpisodeNumber(text: String): Float? {
|
||||||
|
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||||
|
|
||||||
|
return if (episodeMatcher.find()) {
|
||||||
|
if (episodeMatcher.group(2) != null) {
|
||||||
|
episodeMatcher.group(2)?.toFloat()
|
||||||
|
} else {
|
||||||
|
val failedEpisodeNumberPattern: Pattern =
|
||||||
|
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val failedEpisodeNumberMatcher: Matcher =
|
||||||
|
failedEpisodeNumberPattern.matcher(text)
|
||||||
|
if (failedEpisodeNumberMatcher.find()) {
|
||||||
|
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeEpisodeNumber(text: String): String {
|
||||||
|
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||||
|
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
val letterPattern = Regex("[a-zA-Z]")
|
||||||
|
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||||
|
removedNumber
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun removeEpisodeNumberCompletely(text: String): String {
|
||||||
|
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||||
|
val removedNumber = text.replace(regexPattern, "")
|
||||||
|
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||||
|
val failedEpisodeNumberPattern: Regex =
|
||||||
|
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
||||||
|
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||||
|
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removedNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,41 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ImageView
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
|
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||||
|
import ani.dantotsu.others.LanguageMapper
|
||||||
|
import ani.dantotsu.others.webview.CookieCatcher
|
||||||
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
|
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||||
import ani.dantotsu.parsers.WatchSources
|
import ani.dantotsu.parsers.WatchSources
|
||||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class AnimeWatchAdapter(
|
class AnimeWatchAdapter(
|
||||||
private val media: Media,
|
private val media: Media,
|
||||||
private val fragment: AnimeWatchFragment,
|
private val fragment: AnimeWatchFragment,
|
||||||
@@ -38,6 +50,9 @@ class AnimeWatchAdapter(
|
|||||||
return ViewHolder(bind)
|
return ViewHolder(bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var nestedDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
@@ -53,22 +68,44 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
|
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
|
||||||
binding.animeSourceDubbedText.text = if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed)
|
binding.animeSourceDubbedText.text =
|
||||||
|
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
||||||
|
R.string.subbed
|
||||||
|
)
|
||||||
|
|
||||||
//PreferDub
|
//PreferDub
|
||||||
var changing = false
|
var changing = false
|
||||||
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
|
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
|
||||||
binding.animeSourceDubbedText.text = if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed)
|
binding.animeSourceDubbedText.text =
|
||||||
|
if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
||||||
|
R.string.subbed
|
||||||
|
)
|
||||||
if (!changing) fragment.onDubClicked(isChecked)
|
if (!changing) fragment.onDubClicked(isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Wrong Title
|
//Wrong Title
|
||||||
binding.animeSourceSearch.setOnClickListener {
|
binding.animeSourceSearch.setOnClickListener {
|
||||||
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null)
|
SourceSearchDialogFragment().show(
|
||||||
|
fragment.requireActivity().supportFragmentManager,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
|
||||||
|
"Dantotsu",
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
?.getBoolean("offlineMode", false) == true
|
||||||
|
) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
|
binding.animeSourceNameContainer.visibility = offline
|
||||||
|
binding.animeSourceSettings.visibility = offline
|
||||||
|
binding.animeSourceSearch.visibility = offline
|
||||||
|
binding.animeSourceTitle.visibility = offline
|
||||||
|
|
||||||
//Source Selection
|
//Source Selection
|
||||||
val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
var source =
|
||||||
|
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||||
|
setLanguageList(media.selected!!.langIndex, source)
|
||||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||||
binding.animeSource.setText(watchSources.names[source])
|
binding.animeSource.setText(watchSources.names[source])
|
||||||
watchSources[source].apply {
|
watchSources[source].apply {
|
||||||
@@ -80,7 +117,13 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, watchSources.names))
|
binding.animeSource.setAdapter(
|
||||||
|
ArrayAdapter(
|
||||||
|
fragment.requireContext(),
|
||||||
|
R.layout.item_dropdown,
|
||||||
|
watchSources.names
|
||||||
|
)
|
||||||
|
)
|
||||||
binding.animeSourceTitle.isSelected = true
|
binding.animeSourceTitle.isSelected = true
|
||||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||||
fragment.onSourceChange(i).apply {
|
fragment.onSourceChange(i).apply {
|
||||||
@@ -89,13 +132,47 @@ class AnimeWatchAdapter(
|
|||||||
changing = true
|
changing = true
|
||||||
binding.animeSourceDubbed.isChecked = selectDub
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
changing = false
|
changing = false
|
||||||
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
binding.animeSourceDubbedCont.visibility =
|
||||||
|
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||||
|
source = i
|
||||||
|
setLanguageList(0, i)
|
||||||
}
|
}
|
||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
fragment.loadEpisodes(i)
|
fragment.loadEpisodes(i, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Subscription
|
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||||
|
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||||
|
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||||
|
ext.sourceLanguage = i
|
||||||
|
fragment.onLangChange(i)
|
||||||
|
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||||
|
binding.animeSourceTitle.text = showUserText
|
||||||
|
showUserTextListener =
|
||||||
|
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
|
changing = true
|
||||||
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
|
changing = false
|
||||||
|
binding.animeSourceDubbedCont.visibility =
|
||||||
|
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||||
|
setLanguageList(i, source)
|
||||||
|
}
|
||||||
|
subscribeButton(false)
|
||||||
|
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
|
||||||
|
} ?: run {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//settings
|
||||||
|
binding.animeSourceSettings.setOnClickListener {
|
||||||
|
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||||
|
fragment.openSettings(ext.extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Icons
|
||||||
|
|
||||||
|
//subscribe
|
||||||
subscribe = MediaDetailsActivity.PopImageButton(
|
subscribe = MediaDetailsActivity.PopImageButton(
|
||||||
fragment.lifecycleScope,
|
fragment.lifecycleScope,
|
||||||
binding.animeSourceSubscribe,
|
binding.animeSourceSubscribe,
|
||||||
@@ -114,43 +191,99 @@ class AnimeWatchAdapter(
|
|||||||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
//Icons
|
//Nested Button
|
||||||
|
binding.animeNestedButton.setOnClickListener {
|
||||||
|
val dialogView =
|
||||||
|
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||||
|
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||||
|
var refresh = false
|
||||||
|
var run = false
|
||||||
var reversed = media.selected!!.recyclerReversed
|
var reversed = media.selected!!.recyclerReversed
|
||||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
||||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||||
binding.animeSourceTop.setOnClickListener {
|
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||||
|
dialogBinding.animeSourceTop.setOnClickListener {
|
||||||
reversed = !reversed
|
reversed = !reversed
|
||||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||||
|
run = true
|
||||||
}
|
}
|
||||||
|
//Grids
|
||||||
var selected = when (style) {
|
var selected = when (style) {
|
||||||
0 -> binding.animeSourceList
|
0 -> dialogBinding.animeSourceList
|
||||||
1 -> binding.animeSourceGrid
|
1 -> dialogBinding.animeSourceGrid
|
||||||
2 -> binding.animeSourceCompact
|
2 -> dialogBinding.animeSourceCompact
|
||||||
else -> binding.animeSourceList
|
else -> dialogBinding.animeSourceList
|
||||||
|
}
|
||||||
|
when (style) {
|
||||||
|
0 -> dialogBinding.layoutText.text = "List"
|
||||||
|
1 -> dialogBinding.layoutText.text = "Grid"
|
||||||
|
2 -> dialogBinding.layoutText.text = "Compact"
|
||||||
|
else -> dialogBinding.animeSourceList
|
||||||
}
|
}
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
fun selected(it: ImageView) {
|
fun selected(it: ImageButton) {
|
||||||
selected.alpha = 0.33f
|
selected.alpha = 0.33f
|
||||||
selected = it
|
selected = it
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
}
|
}
|
||||||
binding.animeSourceList.setOnClickListener {
|
dialogBinding.animeSourceList.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageButton)
|
||||||
style = 0
|
style = 0
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.layoutText.text = "List"
|
||||||
|
run = true
|
||||||
}
|
}
|
||||||
binding.animeSourceGrid.setOnClickListener {
|
dialogBinding.animeSourceGrid.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageButton)
|
||||||
style = 1
|
style = 1
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.layoutText.text = "Grid"
|
||||||
|
run = true
|
||||||
}
|
}
|
||||||
binding.animeSourceCompact.setOnClickListener {
|
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageButton)
|
||||||
style = 2
|
style = 2
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.layoutText.text = "Compact"
|
||||||
|
run = true
|
||||||
|
}
|
||||||
|
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||||
|
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||||
|
toast("WebView not installed")
|
||||||
|
}
|
||||||
|
//start CookieCatcher activity
|
||||||
|
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||||
|
val sourceAHH = watchSources[source] as? DynamicAnimeParser
|
||||||
|
val sourceHttp =
|
||||||
|
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
|
||||||
|
val url = sourceHttp?.baseUrl
|
||||||
|
url?.let {
|
||||||
|
refresh = true
|
||||||
|
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||||
|
.putExtra("url", url)
|
||||||
|
startActivity(fragment.requireContext(), intent, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//hidden
|
||||||
|
dialogBinding.animeScanlatorContainer.visibility = View.GONE
|
||||||
|
dialogBinding.animeDownloadContainer.visibility = View.GONE
|
||||||
|
|
||||||
|
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
|
||||||
|
.setTitle("Options")
|
||||||
|
.setView(dialogView)
|
||||||
|
.setPositiveButton("OK") { _, _ ->
|
||||||
|
if (run) fragment.onIconPressed(style, reversed)
|
||||||
|
if (refresh) fragment.loadEpisodes(source, true)
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { _, _ ->
|
||||||
|
if (refresh) fragment.loadEpisodes(source, true)
|
||||||
|
}
|
||||||
|
.setOnCancelListener {
|
||||||
|
if (refresh) fragment.loadEpisodes(source, true)
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
nestedDialog?.show()
|
||||||
|
}
|
||||||
//Episode Handling
|
//Episode Handling
|
||||||
handleEpisodes()
|
handleEpisodes()
|
||||||
}
|
}
|
||||||
@@ -169,13 +302,26 @@ class AnimeWatchAdapter(
|
|||||||
for (position in arr.indices) {
|
for (position in arr.indices) {
|
||||||
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
||||||
val chip =
|
val chip =
|
||||||
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root
|
ItemChipBinding.inflate(
|
||||||
|
LayoutInflater.from(fragment.context),
|
||||||
|
binding.animeSourceChipGroup,
|
||||||
|
false
|
||||||
|
).root
|
||||||
chip.isCheckable = true
|
chip.isCheckable = true
|
||||||
fun selected() {
|
fun selected() {
|
||||||
chip.isChecked = true
|
chip.isChecked = true
|
||||||
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
binding.animeWatchChipScroll.smoothScrollTo(
|
||||||
|
(chip.left - screenWidth / 2) + (chip.width / 2),
|
||||||
|
0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||||
|
chip.setTextColor(
|
||||||
|
ContextCompat.getColorStateList(
|
||||||
|
fragment.requireContext(),
|
||||||
|
R.color.chip_text_color
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
selected()
|
selected()
|
||||||
@@ -188,7 +334,14 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (select != null)
|
if (select != null)
|
||||||
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } }
|
binding.animeWatchChipScroll.apply {
|
||||||
|
post {
|
||||||
|
scrollTo(
|
||||||
|
(select.left - screenWidth / 2) + (select.width / 2),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,10 +383,15 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val ep = media.anime.episodes!![continueEp]!!
|
val ep = media.anime.episodes!![continueEp]!!
|
||||||
binding.itemEpisodeImage.loadImage(ep.thumb ?: FileUrl[media.banner ?: media.cover], 0)
|
|
||||||
|
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||||
|
|
||||||
|
binding.itemEpisodeImage.loadImage(
|
||||||
|
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
||||||
|
)
|
||||||
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
||||||
binding.animeSourceContinueText.text =
|
binding.animeSourceContinueText.text =
|
||||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
|
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
|
||||||
binding.animeSourceContinue.setOnClickListener {
|
binding.animeSourceContinue.setOnClickListener {
|
||||||
fragment.onEpisodeClick(continueEp)
|
fragment.onEpisodeClick(continueEp)
|
||||||
}
|
}
|
||||||
@@ -246,6 +404,7 @@ class AnimeWatchAdapter(
|
|||||||
} else {
|
} else {
|
||||||
binding.animeSourceContinue.visibility = View.GONE
|
binding.animeSourceContinue.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSourceProgressBar.visibility = View.GONE
|
binding.animeSourceProgressBar.visibility = View.GONE
|
||||||
if (media.anime.episodes!!.isNotEmpty())
|
if (media.anime.episodes!!.isNotEmpty())
|
||||||
binding.animeSourceNotFound.visibility = View.GONE
|
binding.animeSourceNotFound.visibility = View.GONE
|
||||||
@@ -260,9 +419,40 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setLanguageList(lang: Int, source: Int) {
|
||||||
|
val binding = _binding
|
||||||
|
if (watchSources is AnimeSources) {
|
||||||
|
val parser = watchSources[source] as? DynamicAnimeParser
|
||||||
|
if (parser != null) {
|
||||||
|
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||||
|
ext.sourceLanguage = lang
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||||
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
binding?.animeSourceLanguage?.setText(
|
||||||
|
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val adapter = ArrayAdapter(
|
||||||
|
fragment.requireContext(),
|
||||||
|
R.layout.item_dropdown,
|
||||||
|
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||||
|
)
|
||||||
|
val items = adapter.count
|
||||||
|
|
||||||
|
binding?.animeSourceLanguageContainer?.visibility =
|
||||||
|
if (items > 1) View.VISIBLE else View.GONE
|
||||||
|
binding?.animeSourceLanguage?.setAdapter(adapter)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
//Timer
|
//Timer
|
||||||
countDown(media, binding.animeSourceContainer)
|
countDown(media, binding.animeSourceContainer)
|
||||||
|
|||||||
@@ -1,36 +1,62 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.math.MathUtils
|
import androidx.core.math.MathUtils
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadService
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
|
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.others.LanguageMapper
|
||||||
import ani.dantotsu.parsers.AnimeParser
|
import ani.dantotsu.parsers.AnimeParser
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.HAnimeSources
|
import ani.dantotsu.parsers.HAnimeSources
|
||||||
import ani.dantotsu.settings.PlayerSettings
|
import ani.dantotsu.settings.PlayerSettings
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||||
import ani.dantotsu.subcriptions.Notifications
|
import ani.dantotsu.subcriptions.Notifications
|
||||||
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -50,6 +76,8 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
private lateinit var headerAdapter: AnimeWatchAdapter
|
private lateinit var headerAdapter: AnimeWatchAdapter
|
||||||
private lateinit var episodeAdapter: EpisodeAdapter
|
private lateinit var episodeAdapter: EpisodeAdapter
|
||||||
|
|
||||||
|
val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
var screenWidth = 0f
|
var screenWidth = 0f
|
||||||
private var progress = View.VISIBLE
|
private var progress = View.VISIBLE
|
||||||
|
|
||||||
@@ -69,6 +97,21 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val intentFilter = IntentFilter().apply {
|
||||||
|
addAction(ACTION_DOWNLOAD_STARTED)
|
||||||
|
addAction(ACTION_DOWNLOAD_FINISHED)
|
||||||
|
addAction(ACTION_DOWNLOAD_FAILED)
|
||||||
|
addAction(ACTION_DOWNLOAD_PROGRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
requireContext(),
|
||||||
|
downloadStatusReceiver,
|
||||||
|
intentFilter,
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||||
|
|
||||||
@@ -76,8 +119,10 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
||||||
|
|
||||||
playerSettings =
|
playerSettings =
|
||||||
loadData("player_settings", toast = false) ?: PlayerSettings().apply { saveData("player_settings", this) }
|
loadData("player_settings", toast = false)
|
||||||
uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
?: PlayerSettings().apply { saveData("player_settings", this) }
|
||||||
|
uiSettings = loadData("ui_settings", toast = false)
|
||||||
|
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||||
|
|
||||||
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
|
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
|
||||||
|
|
||||||
@@ -109,7 +154,8 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
media = it
|
media = it
|
||||||
media.selected = model.loadSelected(media)
|
media.selected = model.loadSelected(media)
|
||||||
|
|
||||||
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
subscribed =
|
||||||
|
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
||||||
|
|
||||||
style = media.selected!!.recyclerStyle
|
style = media.selected!!.recyclerStyle
|
||||||
reverse = media.selected!!.recyclerReversed
|
reverse = media.selected!!.recyclerReversed
|
||||||
@@ -120,10 +166,20 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
||||||
|
|
||||||
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
|
val offlineMode =
|
||||||
episodeAdapter = EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
|
model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
|
||||||
|
|
||||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, episodeAdapter)
|
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
|
||||||
|
episodeAdapter =
|
||||||
|
EpisodeAdapter(
|
||||||
|
style ?: uiSettings.animeDefaultView,
|
||||||
|
media,
|
||||||
|
this,
|
||||||
|
offlineMode = offlineMode
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.animeSourceRecycler.adapter =
|
||||||
|
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
awaitAll(
|
awaitAll(
|
||||||
@@ -145,15 +201,23 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
episodes.forEach { (i, episode) ->
|
episodes.forEach { (i, episode) ->
|
||||||
if (media.anime?.fillerEpisodes != null) {
|
if (media.anime?.fillerEpisodes != null) {
|
||||||
if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
|
if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
|
||||||
episode.title = episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
|
episode.title =
|
||||||
|
episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
|
||||||
episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false
|
episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (media.anime?.kitsuEpisodes != null) {
|
if (media.anime?.kitsuEpisodes != null) {
|
||||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||||
episode.desc = episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
|
episode.desc =
|
||||||
episode.title = episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
|
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||||
episode.thumb = episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ?: FileUrl[media.cover]
|
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
|
||||||
|
episode.title ?: ""
|
||||||
|
).isBlank()
|
||||||
|
) media.anime!!.kitsuEpisodes!![i]?.title
|
||||||
|
?: episode.title else episode.title
|
||||||
|
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
|
||||||
|
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
|
||||||
|
?: FileUrl[media.cover]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,17 +278,29 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
return model.watchSources?.get(i)!!
|
return model.watchSources?.get(i)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onLangChange(i: Int) {
|
||||||
|
val selected = model.loadSelected(media)
|
||||||
|
selected.langIndex = i
|
||||||
|
model.saveSelected(media.id, selected, requireActivity())
|
||||||
|
media.selected = selected
|
||||||
|
}
|
||||||
|
|
||||||
fun onDubClicked(checked: Boolean) {
|
fun onDubClicked(checked: Boolean) {
|
||||||
val selected = model.loadSelected(media)
|
val selected = model.loadSelected(media)
|
||||||
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
||||||
selected.preferDub = checked
|
selected.preferDub = checked
|
||||||
model.saveSelected(media.id, selected, requireActivity())
|
model.saveSelected(media.id, selected, requireActivity())
|
||||||
media.selected = selected
|
media.selected = selected
|
||||||
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
model.forceLoadEpisode(
|
||||||
|
media,
|
||||||
|
selected.sourceIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadEpisodes(i: Int) {
|
fun loadEpisodes(i: Int, invalidate: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) }
|
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onIconPressed(viewType: Int, rev: Boolean) {
|
fun onIconPressed(viewType: Int, rev: Boolean) {
|
||||||
@@ -263,22 +339,191 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openSettings(pkg: AnimeExtension.Installed) {
|
||||||
|
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||||
|
val activity = activity
|
||||||
|
if (activity is MediaDetailsActivity && isAdded) {
|
||||||
|
val visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||||
|
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||||
|
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||||
|
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||||
|
try {
|
||||||
|
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||||
|
}
|
||||||
|
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||||
|
if (show) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var itemSelected = false
|
||||||
|
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||||
|
if (allSettings.isNotEmpty()) {
|
||||||
|
var selectedSetting = allSettings[0]
|
||||||
|
if (allSettings.size > 1) {
|
||||||
|
val names =
|
||||||
|
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||||
|
var selectedIndex = 0
|
||||||
|
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||||
|
.setTitle("Select a Source")
|
||||||
|
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||||
|
selectedIndex = which
|
||||||
|
selectedSetting = allSettings[selectedIndex]
|
||||||
|
itemSelected = true
|
||||||
|
dialog.dismiss()
|
||||||
|
|
||||||
|
// Move the fragment transaction here
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
val fragment =
|
||||||
|
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||||
|
changeUIVisibility(true)
|
||||||
|
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||||
|
}
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||||
|
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setOnDismissListener {
|
||||||
|
if (!itemSelected) {
|
||||||
|
changeUIVisibility(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
} else {
|
||||||
|
// If there's only one setting, proceed with the fragment transaction
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
val fragment =
|
||||||
|
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||||
|
changeUIVisibility(true)
|
||||||
|
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||||
|
}
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||||
|
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeUIVisibility(false)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onEpisodeClick(i: String) {
|
fun onEpisodeClick(i: String) {
|
||||||
model.continueMedia = false
|
model.continueMedia = false
|
||||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
|
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||||
|
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
||||||
|
val cancelIntent = Intent().apply {
|
||||||
|
action = AnimeDownloaderService.ACTION_CANCEL_DOWNLOAD
|
||||||
|
putExtra(
|
||||||
|
AnimeDownloaderService.EXTRA_TASK_NAME,
|
||||||
|
AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
requireContext().sendBroadcast(cancelIntent)
|
||||||
|
|
||||||
|
// Remove the download from the manager and update the UI
|
||||||
|
downloadManager.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
media.mainName(),
|
||||||
|
i,
|
||||||
|
DownloadedType.Type.ANIME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
episodeAdapter.purgeDownload(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun onAnimeEpisodeRemoveDownloadClick(i: String) {
|
||||||
|
downloadManager.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
media.mainName(),
|
||||||
|
i,
|
||||||
|
DownloadedType.Type.ANIME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||||
|
val id = requireContext().getSharedPreferences(
|
||||||
|
ContextCompat.getString(requireContext(), R.string.anime_downloads),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).getString(
|
||||||
|
taskName,
|
||||||
|
""
|
||||||
|
) ?: ""
|
||||||
|
requireContext().getSharedPreferences(
|
||||||
|
ContextCompat.getString(requireContext(), R.string.anime_downloads),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).edit().remove(taskName).apply()
|
||||||
|
DownloadService.sendRemoveDownload(
|
||||||
|
requireContext(),
|
||||||
|
ExoplayerDownloadService::class.java,
|
||||||
|
id,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
episodeAdapter.deleteDownload(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (!this@AnimeWatchFragment::episodeAdapter.isInitialized) return
|
||||||
|
when (intent.action) {
|
||||||
|
ACTION_DOWNLOAD_STARTED -> {
|
||||||
|
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||||
|
chapterNumber?.let { episodeAdapter.startDownload(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_DOWNLOAD_FINISHED -> {
|
||||||
|
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||||
|
chapterNumber?.let { episodeAdapter.stopDownload(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_DOWNLOAD_FAILED -> {
|
||||||
|
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||||
|
chapterNumber?.let {
|
||||||
|
episodeAdapter.purgeDownload(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_DOWNLOAD_PROGRESS -> {
|
||||||
|
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||||
|
val progress = intent.getIntExtra("progress", 0)
|
||||||
|
chapterNumber?.let {
|
||||||
|
episodeAdapter.updateDownloadProgress(it, progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun reload() {
|
private fun reload() {
|
||||||
val selected = model.loadSelected(media)
|
val selected = model.loadSelected(media)
|
||||||
|
|
||||||
//Find latest episode for subscription
|
//Find latest episode for subscription
|
||||||
selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
selected.latest =
|
||||||
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
||||||
|
selected.latest =
|
||||||
|
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||||
|
|
||||||
model.saveSelected(media.id, selected, requireActivity())
|
model.saveSelected(media.id, selected, requireActivity())
|
||||||
headerAdapter.handleEpisodes()
|
headerAdapter.handleEpisodes()
|
||||||
|
val isDownloaded = model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
|
||||||
|
episodeAdapter.offlineMode = isDownloaded
|
||||||
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
|
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
|
||||||
var arr: ArrayList<Episode> = arrayListOf()
|
var arr: ArrayList<Episode> = arrayListOf()
|
||||||
if (media.anime!!.episodes != null) {
|
if (media.anime!!.episodes != null) {
|
||||||
@@ -293,11 +538,20 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
episodeAdapter.arr = arr
|
episodeAdapter.arr = arr
|
||||||
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
|
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
|
||||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||||
|
for (download in downloadManager.animeDownloadedTypes) {
|
||||||
|
if (download.title == media.mainName()) {
|
||||||
|
episodeAdapter.stopDownload(download.chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
model.watchSources?.flushText()
|
model.watchSources?.flushText()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
try {
|
||||||
|
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var state: Parcelable? = null
|
var state: Parcelable? = null
|
||||||
@@ -312,4 +566,12 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
|
||||||
|
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
|
||||||
|
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
|
||||||
|
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
|
||||||
|
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.mediarouter.app.MediaRouteActionProvider
|
||||||
|
import androidx.mediarouter.app.MediaRouteChooserDialog
|
||||||
|
import androidx.mediarouter.app.MediaRouteChooserDialogFragment
|
||||||
|
import androidx.mediarouter.app.MediaRouteControllerDialog
|
||||||
|
import androidx.mediarouter.app.MediaRouteControllerDialogFragment
|
||||||
|
import androidx.mediarouter.app.MediaRouteDialogFactory
|
||||||
|
import ani.dantotsu.R
|
||||||
|
|
||||||
|
class CustomCastProvider(context: Context) : MediaRouteActionProvider(context) {
|
||||||
|
init {
|
||||||
|
dialogFactory = CustomCastThemeFactory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomCastThemeFactory : MediaRouteDialogFactory() {
|
||||||
|
override fun onCreateChooserDialogFragment(): MediaRouteChooserDialogFragment {
|
||||||
|
return CustomMediaRouterChooserDialogFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateControllerDialogFragment(): MediaRouteControllerDialogFragment {
|
||||||
|
return CustomMediaRouteControllerDialogFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomMediaRouterChooserDialogFragment : MediaRouteChooserDialogFragment() {
|
||||||
|
override fun onCreateChooserDialog(
|
||||||
|
context: Context,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): MediaRouteChooserDialog =
|
||||||
|
MediaRouteChooserDialog(context, R.style.MyPopup)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomMediaRouteControllerDialogFragment : MediaRouteControllerDialogFragment() {
|
||||||
|
override fun onCreateControllerDialog(
|
||||||
|
context: Context,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): MediaRouteControllerDialog =
|
||||||
|
MediaRouteControllerDialog(context, R.style.MyPopup)
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ data class Episode(
|
|||||||
var selectedExtractor: String? = null,
|
var selectedExtractor: String? = null,
|
||||||
var selectedVideo: Int = 0,
|
var selectedVideo: Int = 0,
|
||||||
var selectedSubtitle: Int? = -1,
|
var selectedSubtitle: Int? = -1,
|
||||||
var extractors: MutableList<VideoExtractor>?=null,
|
var downloadProgress: String? = null,
|
||||||
|
@Transient var extractors: MutableList<VideoExtractor>? = null,
|
||||||
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
|
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
|
||||||
var allStreams: Boolean = false,
|
var allStreams: Boolean = false,
|
||||||
var watched: Long? = null,
|
var watched: Long? = null,
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.LinearInterpolator
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.coroutineScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadIndex
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.connections.updateProgress
|
import ani.dantotsu.connections.updateProgress
|
||||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||||
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
|
import ani.dantotsu.download.video.Helper
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.ln
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
|
fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
|
||||||
val curr = loadData<Long>("${mediaId}_${ep}")
|
val curr = loadData<Long>("${mediaId}_${ep}")
|
||||||
@@ -32,17 +46,42 @@ fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
class EpisodeAdapter(
|
class EpisodeAdapter(
|
||||||
private var type: Int,
|
private var type: Int,
|
||||||
private val media: Media,
|
private val media: Media,
|
||||||
private val fragment: AnimeWatchFragment,
|
private val fragment: AnimeWatchFragment,
|
||||||
var arr: List<Episode> = arrayListOf()
|
var arr: List<Episode> = arrayListOf(),
|
||||||
|
var offlineMode: Boolean
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
private lateinit var index: DownloadIndex
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (offlineMode) {
|
||||||
|
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return (when (viewType) {
|
return (when (viewType) {
|
||||||
0 -> EpisodeListViewHolder(ItemEpisodeListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
0 -> EpisodeListViewHolder(
|
||||||
1 -> EpisodeGridViewHolder(ItemEpisodeGridBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
ItemEpisodeListBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
1 -> EpisodeGridViewHolder(
|
||||||
|
ItemEpisodeGridBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
2 -> EpisodeCompactViewHolder(
|
2 -> EpisodeCompactViewHolder(
|
||||||
ItemEpisodeCompactBinding.inflate(
|
ItemEpisodeCompactBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
@@ -50,6 +89,7 @@ class EpisodeAdapter(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -61,18 +101,23 @@ class EpisodeAdapter(
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
val ep = arr[position]
|
val ep = arr[position]
|
||||||
val title =
|
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
|
||||||
"${if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(R.string.episode_singular)} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
|
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||||
|
} else {
|
||||||
|
ep.number
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
when (holder) {
|
when (holder) {
|
||||||
is EpisodeListViewHolder -> {
|
is EpisodeListViewHolder -> {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||||
|
|
||||||
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
val thumb =
|
||||||
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage)
|
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
||||||
|
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
|
||||||
|
.into(binding.itemEpisodeImage)
|
||||||
binding.itemEpisodeNumber.text = ep.number
|
binding.itemEpisodeNumber.text = ep.number
|
||||||
binding.itemEpisodeTitle.text = title
|
binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
|
||||||
|
|
||||||
if (ep.filler) {
|
if (ep.filler) {
|
||||||
binding.itemEpisodeFiller.visibility = View.VISIBLE
|
binding.itemEpisodeFiller.visibility = View.VISIBLE
|
||||||
@@ -81,8 +126,10 @@ class EpisodeAdapter(
|
|||||||
binding.itemEpisodeFiller.visibility = View.GONE
|
binding.itemEpisodeFiller.visibility = View.GONE
|
||||||
binding.itemEpisodeFillerView.visibility = View.GONE
|
binding.itemEpisodeFillerView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
binding.itemEpisodeDesc.visibility = if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
|
binding.itemEpisodeDesc.visibility =
|
||||||
|
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
|
||||||
binding.itemEpisodeDesc.text = ep.desc ?: ""
|
binding.itemEpisodeDesc.text = ep.desc ?: ""
|
||||||
|
holder.bind(ep.number, ep.downloadProgress , ep.desc)
|
||||||
|
|
||||||
if (media.userProgress != null) {
|
if (media.userProgress != null) {
|
||||||
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) {
|
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) {
|
||||||
@@ -114,8 +161,10 @@ class EpisodeAdapter(
|
|||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||||
|
|
||||||
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
val thumb =
|
||||||
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage)
|
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
||||||
|
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
|
||||||
|
.into(binding.itemEpisodeImage)
|
||||||
|
|
||||||
binding.itemEpisodeNumber.text = ep.number
|
binding.itemEpisodeNumber.text = ep.number
|
||||||
binding.itemEpisodeTitle.text = title
|
binding.itemEpisodeTitle.text = title
|
||||||
@@ -155,7 +204,8 @@ class EpisodeAdapter(
|
|||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||||
binding.itemEpisodeNumber.text = ep.number
|
binding.itemEpisodeNumber.text = ep.number
|
||||||
binding.itemEpisodeFillerView.visibility = if (ep.filler) View.VISIBLE else View.GONE
|
binding.itemEpisodeFillerView.visibility =
|
||||||
|
if (ep.filler) View.VISIBLE else View.GONE
|
||||||
if (media.userProgress != null) {
|
if (media.userProgress != null) {
|
||||||
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
|
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
|
||||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||||
@@ -180,7 +230,82 @@ class EpisodeAdapter(
|
|||||||
|
|
||||||
override fun getItemCount(): Int = arr.size
|
override fun getItemCount(): Int = arr.size
|
||||||
|
|
||||||
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) {
|
private val activeDownloads = mutableSetOf<String>()
|
||||||
|
private val downloadedEpisodes = mutableSetOf<String>()
|
||||||
|
|
||||||
|
fun startDownload(episodeNumber: String) {
|
||||||
|
activeDownloads.add(episodeNumber)
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun stopDownload(episodeNumber: String) {
|
||||||
|
activeDownloads.remove(episodeNumber)
|
||||||
|
downloadedEpisodes.add(episodeNumber)
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
|
||||||
|
media.mainName(),
|
||||||
|
episodeNumber
|
||||||
|
)
|
||||||
|
val id = fragment.requireContext().getSharedPreferences(
|
||||||
|
ContextCompat.getString(fragment.requireContext(), R.string.anime_downloads),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).getString(
|
||||||
|
taskName,
|
||||||
|
""
|
||||||
|
) ?: ""
|
||||||
|
val size = try {
|
||||||
|
val download = index.getDownload(id)
|
||||||
|
bytesToHuman(download?.bytesDownloaded ?: 0)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
arr[position].downloadProgress = "Downloaded" + if (size != null) ": ($size)" else ""
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDownload(episodeNumber: String) {
|
||||||
|
downloadedEpisodes.remove(episodeNumber)
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
arr[position].downloadProgress = null
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun purgeDownload(episodeNumber: String) {
|
||||||
|
activeDownloads.remove(episodeNumber)
|
||||||
|
downloadedEpisodes.remove(episodeNumber)
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
arr[position].downloadProgress = "Failed"
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloadProgress(episodeNumber: String, progress: Int) {
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
arr[position].downloadProgress = "Downloading: $progress%"
|
||||||
|
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
||||||
@@ -189,7 +314,8 @@ class EpisodeAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
||||||
@@ -198,12 +324,38 @@ class EpisodeAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
private val activeCoroutines = mutableSetOf<String>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
||||||
fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
|
fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
|
||||||
}
|
}
|
||||||
|
binding.itemDownload.setOnClickListener {
|
||||||
|
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
|
||||||
|
val episodeNumber = arr[bindingAdapterPosition].number
|
||||||
|
if (activeDownloads.contains(episodeNumber)) {
|
||||||
|
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
|
||||||
|
return@setOnClickListener
|
||||||
|
} else if (downloadedEpisodes.contains(episodeNumber)) {
|
||||||
|
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||||
|
builder.setTitle("Delete Episode")
|
||||||
|
builder.setMessage("Are you sure you want to delete Episode ${episodeNumber}?")
|
||||||
|
builder.setPositiveButton("Yes") { _, _ ->
|
||||||
|
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
|
||||||
|
}
|
||||||
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
|
}
|
||||||
|
val dialog = builder.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
return@setOnClickListener
|
||||||
|
} else {
|
||||||
|
fragment.onAnimeEpisodeDownloadClick(episodeNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
binding.itemEpisodeDesc.setOnClickListener {
|
binding.itemEpisodeDesc.setOnClickListener {
|
||||||
if (binding.itemEpisodeDesc.maxLines == 3)
|
if (binding.itemEpisodeDesc.maxLines == 3)
|
||||||
binding.itemEpisodeDesc.maxLines = 100
|
binding.itemEpisodeDesc.maxLines = 100
|
||||||
@@ -211,11 +363,77 @@ class EpisodeAdapter(
|
|||||||
binding.itemEpisodeDesc.maxLines = 3
|
binding.itemEpisodeDesc.maxLines = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun bind(episodeNumber: String, progress: String?, desc: String?) {
|
||||||
|
if (progress != null) {
|
||||||
|
binding.itemEpisodeDesc.visibility = View.GONE
|
||||||
|
binding.itemDownloadStatus.visibility = View.VISIBLE
|
||||||
|
binding.itemDownloadStatus.text = progress
|
||||||
|
} else {
|
||||||
|
binding.itemDownloadStatus.visibility = View.GONE
|
||||||
|
binding.itemDownloadStatus.text = ""
|
||||||
|
}
|
||||||
|
if (activeDownloads.contains(episodeNumber)) {
|
||||||
|
// Show spinner
|
||||||
|
binding.itemDownload.setImageResource(R.drawable.ic_sync)
|
||||||
|
startOrContinueRotation(episodeNumber)
|
||||||
|
binding.itemEpisodeDesc.visibility = View.GONE
|
||||||
|
} else if (downloadedEpisodes.contains(episodeNumber)) {
|
||||||
|
binding.itemEpisodeDesc.visibility = View.GONE
|
||||||
|
binding.itemDownloadStatus.visibility = View.VISIBLE
|
||||||
|
// Show checkmark
|
||||||
|
binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
|
||||||
|
//binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places
|
||||||
|
binding.itemDownload.postDelayed({
|
||||||
|
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
|
||||||
|
binding.itemDownload.rotation = 0f
|
||||||
|
//binding.itemDownload.setColorFilter(typedValue2.data)
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
binding.itemDownloadStatus.visibility = View.GONE
|
||||||
|
binding.itemEpisodeDesc.visibility = if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE
|
||||||
|
// Show download icon
|
||||||
|
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
|
||||||
|
binding.itemDownload.rotation = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startOrContinueRotation(episodeNumber: String) {
|
||||||
|
if (!isRotationCoroutineRunningFor(episodeNumber)) {
|
||||||
|
val scope = fragment.lifecycle.coroutineScope
|
||||||
|
scope.launch {
|
||||||
|
// Add chapter number to active coroutines set
|
||||||
|
activeCoroutines.add(episodeNumber)
|
||||||
|
while (activeDownloads.contains(episodeNumber)) {
|
||||||
|
binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
|
||||||
|
.setInterpolator(
|
||||||
|
LinearInterpolator()
|
||||||
|
).start()
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
// Remove chapter number from active coroutines set
|
||||||
|
activeCoroutines.remove(episodeNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isRotationCoroutineRunningFor(episodeNumber: String): Boolean {
|
||||||
|
return episodeNumber in activeCoroutines
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateType(t: Int) {
|
fun updateType(t: Int) {
|
||||||
type = t
|
type = t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun bytesToHuman(bytes: Long): String? {
|
||||||
|
if (bytes < 0) return null
|
||||||
|
val unit = 1000
|
||||||
|
if (bytes < unit) return "$bytes B"
|
||||||
|
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||||
|
val pre = ("KMGTPE")[exp - 1]
|
||||||
|
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,13 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -18,17 +21,21 @@ import ani.dantotsu.*
|
|||||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||||
import ani.dantotsu.databinding.ItemStreamBinding
|
import ani.dantotsu.databinding.ItemStreamBinding
|
||||||
import ani.dantotsu.databinding.ItemUrlBinding
|
import ani.dantotsu.databinding.ItemUrlBinding
|
||||||
|
import ani.dantotsu.download.video.Helper
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.others.Download.download
|
import ani.dantotsu.others.Download.download
|
||||||
|
import ani.dantotsu.parsers.Subtitle
|
||||||
import ani.dantotsu.parsers.VideoExtractor
|
import ani.dantotsu.parsers.VideoExtractor
|
||||||
import ani.dantotsu.parsers.VideoType
|
import ani.dantotsu.parsers.VideoType
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
|
|
||||||
class SelectorDialogFragment : BottomSheetDialogFragment() {
|
class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||||
private var _binding: BottomSheetSelectorBinding? = null
|
private var _binding: BottomSheetSelectorBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
@@ -40,6 +47,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
private var makeDefault = false
|
private var makeDefault = false
|
||||||
private var selected: String? = null
|
private var selected: String? = null
|
||||||
private var launch: Boolean? = null
|
private var launch: Boolean? = null
|
||||||
|
private var isDownloadMenu: Boolean? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -47,11 +55,22 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
selected = it.getString("server")
|
selected = it.getString("server")
|
||||||
launch = it.getBoolean("launch", true)
|
launch = it.getBoolean("launch", true)
|
||||||
prevEpisode = it.getString("prev")
|
prevEpisode = it.getString("prev")
|
||||||
|
isDownloadMenu = it.getBoolean("isDownload")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
||||||
|
val window = dialog?.window
|
||||||
|
window?.statusBarColor = Color.TRANSPARENT
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
val theme = requireContext().theme
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||||
|
window?.navigationBarColor = typedValue.data
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +83,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
|
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
|
||||||
episode = ep
|
episode = ep
|
||||||
if (ep != null) {
|
if (ep != null) {
|
||||||
|
if (isDownloadMenu == true) {
|
||||||
|
binding.selectorMakeDefault.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
if (selected != null) {
|
if (selected != null && isDownloadMenu == false) {
|
||||||
binding.selectorListContainer.visibility = View.GONE
|
binding.selectorListContainer.visibility = View.GONE
|
||||||
binding.selectorAutoListContainer.visibility = View.VISIBLE
|
binding.selectorAutoListContainer.visibility = View.VISIBLE
|
||||||
binding.selectorAutoText.text = selected
|
binding.selectorAutoText.text = selected
|
||||||
@@ -82,10 +104,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
val size = ep.extractors?.find { it.server.name == selected }?.videos?.size
|
val size =
|
||||||
|
if (model.watchSources!!.isDownloadedSource(media!!.selected!!.sourceIndex)) {
|
||||||
|
ep.extractors?.firstOrNull()?.videos?.size
|
||||||
|
} else {
|
||||||
|
ep.extractors?.find { it.server.name == selected }?.videos?.size
|
||||||
|
}
|
||||||
|
|
||||||
if (size != null && size >= media!!.selected!!.video) {
|
if (size != null && size >= media!!.selected!!.video) {
|
||||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = selected
|
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
|
||||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo = media!!.selected!!.video
|
selected
|
||||||
|
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo =
|
||||||
|
media!!.selected!!.video
|
||||||
startExoplayer(media!!)
|
startExoplayer(media!!)
|
||||||
} else fail()
|
} else fail()
|
||||||
}
|
}
|
||||||
@@ -106,8 +136,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}) fail()
|
}) fail()
|
||||||
}
|
}
|
||||||
} else load()
|
} else load()
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
binding.selectorRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.selectorRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = navBarHeight
|
bottomMargin = navBarHeight
|
||||||
}
|
}
|
||||||
@@ -120,18 +149,28 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
saveData("make_default", makeDefault)
|
saveData("make_default", makeDefault)
|
||||||
}
|
}
|
||||||
binding.selectorRecyclerView.layoutManager =
|
binding.selectorRecyclerView.layoutManager =
|
||||||
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false)
|
LinearLayoutManager(
|
||||||
|
requireActivity(),
|
||||||
|
LinearLayoutManager.VERTICAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
val adapter = ExtractorAdapter()
|
val adapter = ExtractorAdapter()
|
||||||
binding.selectorRecyclerView.adapter = adapter
|
binding.selectorRecyclerView.adapter = adapter
|
||||||
if (!ep.allStreams) {
|
if (!ep.allStreams) {
|
||||||
ep.extractorCallback = {
|
ep.extractorCallback = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
adapter.add(it)
|
adapter.add(it)
|
||||||
|
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
|
||||||
|
adapter.performClick(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model.getEpisode().observe(this) {
|
model.getEpisode().observe(this) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
|
media!!.anime?.episodes?.set(
|
||||||
|
media!!.anime?.selectedEpisode!!,
|
||||||
|
ep
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
@@ -143,6 +182,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
} else {
|
} else {
|
||||||
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
|
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
|
||||||
adapter.addAll(ep.extractors)
|
adapter.addAll(ep.extractors)
|
||||||
|
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
|
||||||
|
adapter.performClick(0)
|
||||||
|
}
|
||||||
binding.selectorProgressBar.visibility = View.GONE
|
binding.selectorProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,14 +200,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
prevEpisode = null
|
prevEpisode = null
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
if (launch!!) {
|
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
||||||
stopAddingToList()
|
stopAddingToList()
|
||||||
val intent = Intent(activity, ExoplayerView::class.java)
|
val intent = Intent(activity, ExoplayerView::class.java)
|
||||||
ExoplayerView.media = media
|
ExoplayerView.media = media
|
||||||
ExoplayerView.initialized = true
|
ExoplayerView.initialized = true
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
model.setEpisode(media.anime!!.episodes!![media.anime.selectedEpisode!!]!!, "startExo no launch")
|
model.setEpisode(
|
||||||
|
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
|
||||||
|
"startExo no launch"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,14 +221,22 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ExtractorAdapter : RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() {
|
private inner class ExtractorAdapter :
|
||||||
|
RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() {
|
||||||
val links = mutableListOf<VideoExtractor>()
|
val links = mutableListOf<VideoExtractor>()
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
||||||
StreamViewHolder(ItemStreamBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
StreamViewHolder(
|
||||||
|
ItemStreamBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||||
val extractor = links[position]
|
val extractor = links[position]
|
||||||
holder.binding.streamName.text = extractor.server.name
|
holder.binding.streamName.text = ""//extractor.server.name
|
||||||
|
holder.binding.streamName.visibility = View.GONE
|
||||||
|
|
||||||
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
|
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
|
||||||
@@ -204,55 +257,168 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
notifyItemRangeInserted(0, extractors.size)
|
notifyItemRangeInserted(0, extractors.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class StreamViewHolder(val binding: ItemStreamBinding) : RecyclerView.ViewHolder(binding.root)
|
fun performClick(position: Int) {
|
||||||
|
try { //bandaid fix for crash
|
||||||
|
val extractor = links[position]
|
||||||
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
|
||||||
|
extractor.server.name
|
||||||
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = 0
|
||||||
|
startExoplayer(media!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class VideoAdapter(private val extractor : VideoExtractor) : RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() {
|
private inner class StreamViewHolder(val binding: ItemStreamBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class VideoAdapter(private val extractor: VideoExtractor) :
|
||||||
|
RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder {
|
||||||
return UrlViewHolder(ItemUrlBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
return UrlViewHolder(
|
||||||
|
ItemUrlBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val video = extractor.videos[position]
|
val video = extractor.videos[position]
|
||||||
binding.urlQuality.text = if(video.quality!=null) "${video.quality}p" else "Default Quality"
|
if (isDownloadMenu == true) {
|
||||||
binding.urlNote.text = video.extraNote ?: ""
|
|
||||||
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
|
|
||||||
binding.urlDownload.visibility = View.VISIBLE
|
binding.urlDownload.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.urlDownload.visibility = View.GONE
|
||||||
|
}
|
||||||
binding.urlDownload.setSafeOnClickListener {
|
binding.urlDownload.setSafeOnClickListener {
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = extractor.server.name
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = position
|
extractor.server.name
|
||||||
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
|
||||||
|
position
|
||||||
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
||||||
|
val selectedVideo =
|
||||||
|
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||||
|
|
||||||
|
val subtitles = extractor.subtitles
|
||||||
|
val subtitleNames = subtitles.map { it.language }
|
||||||
|
var subtitleToDownload: Subtitle? = null
|
||||||
|
if (subtitles.isNotEmpty()) {
|
||||||
|
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||||
|
.setTitle("Download Subtitle")
|
||||||
|
.setSingleChoiceItems(
|
||||||
|
subtitleNames.toTypedArray(),
|
||||||
|
-1
|
||||||
|
) { dialog, which ->
|
||||||
|
subtitleToDownload = subtitles[which]
|
||||||
|
}
|
||||||
|
.setPositiveButton("Download") { _, _ ->
|
||||||
|
dialog?.dismiss()
|
||||||
|
if (selectedVideo != null) {
|
||||||
|
Helper.startAnimeDownloadService(
|
||||||
|
currActivity()!!,
|
||||||
|
media!!.mainName(),
|
||||||
|
episode.number,
|
||||||
|
selectedVideo,
|
||||||
|
subtitleToDownload,
|
||||||
|
media,
|
||||||
|
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
snackString("No Video Selected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton("Skip") { dialog, _ ->
|
||||||
|
subtitleToDownload = null
|
||||||
|
if (selectedVideo != null) {
|
||||||
|
Helper.startAnimeDownloadService(
|
||||||
|
currActivity()!!,
|
||||||
|
media!!.mainName(),
|
||||||
|
episode.number,
|
||||||
|
selectedVideo,
|
||||||
|
subtitleToDownload,
|
||||||
|
media,
|
||||||
|
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
snackString("No Video Selected")
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setNeutralButton("Cancel") { dialog, _ ->
|
||||||
|
subtitleToDownload = null
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
alertDialog.window?.setDimAmount(0.8f)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (selectedVideo != null) {
|
||||||
|
Helper.startAnimeDownloadService(
|
||||||
|
requireActivity(),
|
||||||
|
media!!.mainName(),
|
||||||
|
episode.number,
|
||||||
|
selectedVideo,
|
||||||
|
subtitleToDownload,
|
||||||
|
media,
|
||||||
|
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
snackString("No Video Selected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
binding.urlDownload.setOnLongClickListener {
|
||||||
|
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
if ((loadData<Int>("settings_download_manager") ?: 0) != 0) {
|
||||||
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||||
|
extractor.server.name
|
||||||
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
|
||||||
|
position
|
||||||
download(
|
download(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
||||||
media!!.userPreferredName
|
media!!.userPreferredName
|
||||||
)
|
)
|
||||||
dismiss()
|
} else {
|
||||||
|
snackString("No Download Manager Selected")
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
if (video.format == VideoType.CONTAINER) {
|
if (video.format == VideoType.CONTAINER) {
|
||||||
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
|
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
|
||||||
binding.urlSize.text =
|
binding.urlSize.text =
|
||||||
(if (video.extraNote != null) " : " else "") + DecimalFormat("#.##").format(video.size ?: 0).toString() + " MB"
|
// if video size is null or 0, show "Unknown Size" else show the size in MB
|
||||||
}
|
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
|
||||||
else {
|
"#.##"
|
||||||
binding.urlQuality.text = "Multi Quality"
|
).format(video.size ?: 0).toString() + " MB"))
|
||||||
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
|
|
||||||
binding.urlDownload.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
binding.urlNote.visibility = View.VISIBLE
|
||||||
|
binding.urlNote.text = video.format.name
|
||||||
|
binding.urlQuality.text = extractor.server.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = extractor.videos.size
|
override fun getItemCount(): Int = extractor.videos.size
|
||||||
|
|
||||||
private inner class UrlViewHolder(val binding: ItemUrlBinding) : RecyclerView.ViewHolder(binding.root) {
|
private inner class UrlViewHolder(val binding: ItemUrlBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setSafeOnClickListener {
|
itemView.setSafeOnClickListener {
|
||||||
|
if (isDownloadMenu == true) {
|
||||||
|
binding.urlDownload.performClick()
|
||||||
|
return@setSafeOnClickListener
|
||||||
|
}
|
||||||
tryWith(true) {
|
tryWith(true) {
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = extractor.server.name
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = bindingAdapterPosition
|
extractor.server.name
|
||||||
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo =
|
||||||
|
bindingAdapterPosition
|
||||||
if (makeDefault) {
|
if (makeDefault) {
|
||||||
media!!.selected!!.server = extractor.server.name
|
media!!.selected!!.server = extractor.server.name
|
||||||
media!!.selected!!.video = bindingAdapterPosition
|
media!!.selected!!.video = bindingAdapterPosition
|
||||||
@@ -276,12 +442,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(server: String? = null, la: Boolean = true, prev: String? = null): SelectorDialogFragment =
|
fun newInstance(
|
||||||
|
server: String? = null,
|
||||||
|
la: Boolean = true,
|
||||||
|
prev: String? = null,
|
||||||
|
isDownload: Boolean
|
||||||
|
): SelectorDialogFragment =
|
||||||
SelectorDialogFragment().apply {
|
SelectorDialogFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putString("server", server)
|
putString("server", server)
|
||||||
putBoolean("launch", la)
|
putBoolean("launch", la)
|
||||||
putString("prev", prev)
|
putString("prev", prev)
|
||||||
|
putBoolean("isDownload", isDownload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.BottomSheetDialogFragment
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
@@ -24,7 +26,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||||||
val model: MediaDetailsViewModel by activityViewModels()
|
val model: MediaDetailsViewModel by activityViewModels()
|
||||||
private lateinit var episode: Episode
|
private lateinit var episode: Episode
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
|
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -34,18 +40,29 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe
|
episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe
|
||||||
val currentExtractor = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return@observe
|
val currentExtractor =
|
||||||
|
episode.extractors?.find { it.server.name == episode.selectedExtractor }
|
||||||
|
?: return@observe
|
||||||
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
|
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles)
|
binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class SubtitleAdapter(val subtitles: List<Subtitle>) : RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() {
|
inner class SubtitleAdapter(val subtitles: List<Subtitle>) :
|
||||||
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) : RecyclerView.ViewHolder(binding.root)
|
RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() {
|
||||||
|
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
||||||
StreamViewHolder(ItemSubtitleTextBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
StreamViewHolder(
|
||||||
|
ItemSubtitleTextBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ object VideoCache {
|
|||||||
val databaseProvider = StandaloneDatabaseProvider(context)
|
val databaseProvider = StandaloneDatabaseProvider(context)
|
||||||
if (simpleCache == null)
|
if (simpleCache == null)
|
||||||
simpleCache = SimpleCache(
|
simpleCache = SimpleCache(
|
||||||
File(context.cacheDir, "exoplayer").also { it.deleteOnExit() }, // Ensures always fresh file
|
File(
|
||||||
|
context.cacheDir,
|
||||||
|
"exoplayer"
|
||||||
|
).also { it.deleteOnExit() }, // Ensures always fresh file
|
||||||
LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L),
|
LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L),
|
||||||
databaseProvider
|
databaseProvider
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import android.os.Build
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.snackString
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -21,43 +23,58 @@ data class ImageData(
|
|||||||
val page: Page,
|
val page: Page,
|
||||||
val source: HttpSource
|
val source: HttpSource
|
||||||
) {
|
) {
|
||||||
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? {
|
suspend fun fetchAndProcessImage(
|
||||||
|
page: Page,
|
||||||
|
httpSource: HttpSource,
|
||||||
|
context: Context
|
||||||
|
): Bitmap? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Fetch the image
|
// Fetch the image
|
||||||
val response = httpSource.getImage(page)
|
val response = httpSource.getImage(page)
|
||||||
println("Response: ${response.code}")
|
logger("Response: ${response.code}")
|
||||||
println("Response: ${response.message}")
|
logger("Response: ${response.message}")
|
||||||
|
|
||||||
// Convert the Response to an InputStream
|
// Convert the Response to an InputStream
|
||||||
val inputStream = response.body?.byteStream()
|
val inputStream = response.body.byteStream()
|
||||||
|
|
||||||
// Convert InputStream to Bitmap
|
// Convert InputStream to Bitmap
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
|
||||||
inputStream?.close()
|
inputStream.close()
|
||||||
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
//saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
||||||
|
|
||||||
return@withContext bitmap
|
return@withContext bitmap
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Handle any exceptions
|
// Handle any exceptions
|
||||||
println("An error occurred: ${e.message}")
|
logger("An error occurred: ${e.message}")
|
||||||
|
snackString("An error occurred: ${e.message}")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) {
|
fun saveImage(
|
||||||
|
bitmap: Bitmap,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
filename: String,
|
||||||
|
format: Bitmap.CompressFormat,
|
||||||
|
quality: Int
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val contentValues = ContentValues().apply {
|
val contentValues = ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
|
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga")
|
put(
|
||||||
|
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||||
|
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
val uri: Uri? =
|
||||||
|
contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
|
||||||
uri?.let {
|
uri?.let {
|
||||||
contentResolver.openOutputStream(it)?.use { os ->
|
contentResolver.openOutputStream(it)?.use { os ->
|
||||||
@@ -65,7 +82,8 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
val directory =
|
||||||
|
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga")
|
||||||
if (!directory.exists()) {
|
if (!directory.exists()) {
|
||||||
directory.mkdirs()
|
directory.mkdirs()
|
||||||
}
|
}
|
||||||
@@ -81,7 +99,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaCache() {
|
class MangaCache {
|
||||||
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt()
|
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt()
|
||||||
private val cache = LruCache<String, ImageData>(maxMemory)
|
private val cache = LruCache<String, ImageData>(maxMemory)
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,18 @@ data class MangaChapter(
|
|||||||
var link: String,
|
var link: String,
|
||||||
var title: String? = null,
|
var title: String? = null,
|
||||||
var description: String? = null,
|
var description: String? = null,
|
||||||
var sChapter: SChapter
|
var sChapter: SChapter,
|
||||||
|
val scanlator: String? = null,
|
||||||
|
var progress: String? = ""
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)
|
constructor(chapter: MangaChapter) : this(
|
||||||
|
chapter.number,
|
||||||
|
chapter.link,
|
||||||
|
chapter.title,
|
||||||
|
chapter.description,
|
||||||
|
chapter.sChapter,
|
||||||
|
chapter.scanlator
|
||||||
|
)
|
||||||
|
|
||||||
private val images = mutableListOf<MangaImage>()
|
private val images = mutableListOf<MangaImage>()
|
||||||
fun images(): List<MangaImage> = images
|
fun images(): List<MangaImage> = images
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
package ani.dantotsu.media.manga
|
package ani.dantotsu.media.manga
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.LinearInterpolator
|
||||||
|
import android.widget.NumberPicker
|
||||||
|
import androidx.lifecycle.coroutineScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.connections.updateProgress
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemChapterListBinding
|
import ani.dantotsu.databinding.ItemChapterListBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.setAnimation
|
import ani.dantotsu.setAnimation
|
||||||
import ani.dantotsu.connections.updateProgress
|
import kotlinx.coroutines.delay
|
||||||
import java.util.regex.Matcher
|
import kotlinx.coroutines.launch
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class MangaChapterAdapter(
|
class MangaChapterAdapter(
|
||||||
private var type: Int,
|
private var type: Int,
|
||||||
@@ -28,7 +35,15 @@ class MangaChapterAdapter(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
0 -> ChapterListViewHolder(ItemChapterListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
|
0 -> ChapterListViewHolder(
|
||||||
|
ItemChapterListBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +54,8 @@ class MangaChapterAdapter(
|
|||||||
|
|
||||||
override fun getItemCount(): Int = arr.size
|
override fun getItemCount(): Int = arr.size
|
||||||
|
|
||||||
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||||
@@ -48,12 +64,196 @@ class MangaChapterAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) {
|
private val activeDownloads = mutableSetOf<String>()
|
||||||
|
private val downloadedChapters = mutableSetOf<String>()
|
||||||
|
|
||||||
|
fun startDownload(chapterNumber: String) {
|
||||||
|
activeDownloads.add(chapterNumber)
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopDownload(chapterNumber: String) {
|
||||||
|
activeDownloads.remove(chapterNumber)
|
||||||
|
downloadedChapters.add(chapterNumber)
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
arr[position].progress = "Downloaded"
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDownload(chapterNumber: String) {
|
||||||
|
downloadedChapters.remove(chapterNumber)
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
arr[position].progress = ""
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun purgeDownload(chapterNumber: String) {
|
||||||
|
activeDownloads.remove(chapterNumber)
|
||||||
|
downloadedChapters.remove(chapterNumber)
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
arr[position].progress = ""
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloadProgress(chapterNumber: String, progress: Int) {
|
||||||
|
// Find the position of the chapter and notify only that item
|
||||||
|
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||||
|
if (position != -1) {
|
||||||
|
arr[position].progress = "Downloading: ${progress}%"
|
||||||
|
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadNChaptersFrom(position: Int, n: Int) {
|
||||||
|
//download next n chapters
|
||||||
|
if (position < 0 || position >= arr.size) return
|
||||||
|
for (i in 0..<n) {
|
||||||
|
if (position + i < arr.size) {
|
||||||
|
val chapterNumber = arr[position + i].number
|
||||||
|
if (activeDownloads.contains(chapterNumber)) {
|
||||||
|
//do nothing
|
||||||
|
continue
|
||||||
|
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||||
|
//do nothing
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||||
|
startDownload(chapterNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
private val activeCoroutines = mutableSetOf<String>()
|
||||||
|
private val typedValue1 = TypedValue()
|
||||||
|
private val typedValue2 = TypedValue()
|
||||||
|
fun bind(chapterNumber: String, progress: String?) {
|
||||||
|
if (progress != null) {
|
||||||
|
binding.itemChapterTitle.visibility = View.VISIBLE
|
||||||
|
binding.itemChapterTitle.text = "$progress"
|
||||||
|
} else {
|
||||||
|
binding.itemChapterTitle.visibility = View.GONE
|
||||||
|
binding.itemChapterTitle.text = ""
|
||||||
|
}
|
||||||
|
if (activeDownloads.contains(chapterNumber)) {
|
||||||
|
// Show spinner
|
||||||
|
binding.itemDownload.setImageResource(R.drawable.ic_sync)
|
||||||
|
startOrContinueRotation(chapterNumber)
|
||||||
|
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||||
|
// Show checkmark
|
||||||
|
binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
|
||||||
|
//binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places
|
||||||
|
binding.itemDownload.postDelayed({
|
||||||
|
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
|
||||||
|
binding.itemDownload.rotation = 0f
|
||||||
|
//binding.itemDownload.setColorFilter(typedValue2.data)
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
// Show download icon
|
||||||
|
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
|
||||||
|
binding.itemDownload.rotation = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startOrContinueRotation(chapterNumber: String) {
|
||||||
|
if (!isRotationCoroutineRunningFor(chapterNumber)) {
|
||||||
|
val scope = fragment.lifecycle.coroutineScope
|
||||||
|
scope.launch {
|
||||||
|
// Add chapter number to active coroutines set
|
||||||
|
activeCoroutines.add(chapterNumber)
|
||||||
|
while (activeDownloads.contains(chapterNumber)) {
|
||||||
|
binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
|
||||||
|
.setInterpolator(
|
||||||
|
LinearInterpolator()
|
||||||
|
).start()
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
// Remove chapter number from active coroutines set
|
||||||
|
activeCoroutines.remove(chapterNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isRotationCoroutineRunningFor(chapterNumber: String): Boolean {
|
||||||
|
return chapterNumber in activeCoroutines
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
val theme = currContext()?.theme
|
||||||
|
theme?.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorError,
|
||||||
|
typedValue1,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
theme?.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorPrimary,
|
||||||
|
typedValue2,
|
||||||
|
true
|
||||||
|
)
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
||||||
}
|
}
|
||||||
|
binding.itemDownload.setOnClickListener {
|
||||||
|
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
|
||||||
|
val chapterNumber = arr[bindingAdapterPosition].number
|
||||||
|
if (activeDownloads.contains(chapterNumber)) {
|
||||||
|
fragment.onMangaChapterStopDownloadClick(chapterNumber)
|
||||||
|
return@setOnClickListener
|
||||||
|
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||||
|
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||||
|
builder.setTitle("Delete Chapter")
|
||||||
|
builder.setMessage("Are you sure you want to delete ${chapterNumber}?")
|
||||||
|
builder.setPositiveButton("Yes") { _, _ ->
|
||||||
|
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
|
||||||
|
}
|
||||||
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
|
}
|
||||||
|
val dialog = builder.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
return@setOnClickListener
|
||||||
|
} else {
|
||||||
|
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||||
|
startDownload(chapterNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.itemDownload.setOnLongClickListener {
|
||||||
|
//Alert dialog asking for the number of chapters to download
|
||||||
|
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||||
|
alertDialog.setTitle("Multi Chapter Downloader")
|
||||||
|
alertDialog.setMessage("Enter the number of chapters to download")
|
||||||
|
val input = NumberPicker(currContext())
|
||||||
|
input.minValue = 1
|
||||||
|
input.maxValue = itemCount - bindingAdapterPosition
|
||||||
|
input.value = 1
|
||||||
|
alertDialog.setView(input)
|
||||||
|
alertDialog.setPositiveButton("OK") { _, _ ->
|
||||||
|
downloadNChaptersFrom(bindingAdapterPosition, input.value)
|
||||||
|
}
|
||||||
|
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||||
|
val dialog = alertDialog.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,44 +263,50 @@ class MangaChapterAdapter(
|
|||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||||
val ep = arr[position]
|
val ep = arr[position]
|
||||||
binding.itemEpisodeNumber.text = ep.number
|
val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
|
||||||
|
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
|
||||||
if (media.userProgress != null) {
|
if (media.userProgress != null) {
|
||||||
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat())
|
if ((MangaNameAdapter.findChapterNumber(ep.number)
|
||||||
|
?: 9999f) <= media.userProgress!!.toFloat()
|
||||||
|
)
|
||||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||||
else {
|
else {
|
||||||
binding.itemEpisodeViewedCover.visibility = View.GONE
|
binding.itemEpisodeViewedCover.visibility = View.GONE
|
||||||
binding.itemEpisodeCont.setOnLongClickListener {
|
binding.itemEpisodeCont.setOnLongClickListener {
|
||||||
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString())
|
updateProgress(
|
||||||
|
media,
|
||||||
|
MangaNameAdapter.findChapterNumber(ep.number).toString()
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ChapterListViewHolder -> {
|
is ChapterListViewHolder -> {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val ep = arr[position]
|
val ep = arr[position]
|
||||||
|
holder.bind(ep.number, ep.progress)
|
||||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||||
binding.itemChapterNumber.text = ep.number
|
binding.itemChapterNumber.text = ep.number
|
||||||
if (!ep.title.isNullOrEmpty()) {
|
if (ep.progress.isNullOrEmpty()) {
|
||||||
binding.itemChapterTitle.text = ep.title
|
binding.itemChapterTitle.visibility = View.GONE
|
||||||
binding.itemChapterTitle.setOnLongClickListener {
|
} else binding.itemChapterTitle.visibility = View.VISIBLE
|
||||||
binding.itemChapterTitle.maxLines.apply {
|
|
||||||
binding.itemChapterTitle.maxLines = if (this == 1) 3 else 1
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.itemChapterTitle.visibility = View.VISIBLE
|
|
||||||
} else binding.itemChapterTitle.visibility = View.GONE
|
|
||||||
|
|
||||||
if (media.userProgress != null) {
|
if (media.userProgress != null) {
|
||||||
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat()) {
|
if ((MangaNameAdapter.findChapterNumber(ep.number)
|
||||||
|
?: 9999f) <= media.userProgress!!.toFloat()
|
||||||
|
) {
|
||||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||||
binding.itemEpisodeViewed.visibility = View.VISIBLE
|
binding.itemEpisodeViewed.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
binding.itemEpisodeViewedCover.visibility = View.GONE
|
binding.itemEpisodeViewedCover.visibility = View.GONE
|
||||||
binding.itemEpisodeViewed.visibility = View.GONE
|
binding.itemEpisodeViewed.visibility = View.GONE
|
||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener {
|
||||||
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString())
|
updateProgress(
|
||||||
|
media,
|
||||||
|
MangaNameAdapter.findChapterNumber(ep.number).toString()
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,25 @@ import java.util.regex.Pattern
|
|||||||
|
|
||||||
class MangaNameAdapter {
|
class MangaNameAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
|
const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*"
|
||||||
|
const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
|
||||||
fun findChapterNumber(text: String): Float? {
|
fun findChapterNumber(text: String): Float? {
|
||||||
val regex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)"
|
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
|
||||||
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
|
|
||||||
val matcher: Matcher = pattern.matcher(text)
|
val matcher: Matcher = pattern.matcher(text)
|
||||||
|
|
||||||
return if (matcher.find()) {
|
return if (matcher.find()) {
|
||||||
matcher.group(2)?.toFloat()
|
matcher.group(2)?.toFloat()
|
||||||
|
} else {
|
||||||
|
val failedChapterNumberPattern: Pattern =
|
||||||
|
Pattern.compile(filedChapterNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val failedChapterNumberMatcher: Matcher =
|
||||||
|
failedChapterNumberPattern.matcher(text)
|
||||||
|
if (failedChapterNumberMatcher.find()) {
|
||||||
|
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +1,42 @@
|
|||||||
package ani.dantotsu.media.manga
|
package ani.dantotsu.media.manga
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ImageView
|
import android.widget.CheckBox
|
||||||
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.NumberPicker
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.media.anime.handleProgress
|
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||||
|
import ani.dantotsu.media.anime.handleProgress
|
||||||
|
import ani.dantotsu.others.LanguageMapper
|
||||||
|
import ani.dantotsu.others.webview.CookieCatcher
|
||||||
|
import ani.dantotsu.parsers.DynamicMangaParser
|
||||||
import ani.dantotsu.parsers.MangaReadSources
|
import ani.dantotsu.parsers.MangaReadSources
|
||||||
|
import ani.dantotsu.parsers.MangaSources
|
||||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class MangaReadAdapter(
|
class MangaReadAdapter(
|
||||||
private val media: Media,
|
private val media: Media,
|
||||||
private val fragment: MangaReadFragment,
|
private val fragment: MangaReadFragment,
|
||||||
@@ -31,12 +45,17 @@ class MangaReadAdapter(
|
|||||||
|
|
||||||
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
||||||
private var _binding: ItemAnimeWatchBinding? = null
|
private var _binding: ItemAnimeWatchBinding? = null
|
||||||
|
val hiddenScanlators = mutableListOf<String>()
|
||||||
|
var scanlatorSelectionListener: ScanlatorSelectionListener? = null
|
||||||
|
var options = listOf<String>()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return ViewHolder(bind)
|
return ViewHolder(bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var nestedDialog: AlertDialog? = null
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
@@ -45,31 +64,79 @@ class MangaReadAdapter(
|
|||||||
|
|
||||||
//Wrong Title
|
//Wrong Title
|
||||||
binding.animeSourceSearch.setOnClickListener {
|
binding.animeSourceSearch.setOnClickListener {
|
||||||
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null)
|
SourceSearchDialogFragment().show(
|
||||||
|
fragment.requireActivity().supportFragmentManager,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
|
||||||
|
"Dantotsu",
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
?.getBoolean("offlineMode", false) == true
|
||||||
|
) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
|
binding.animeSourceNameContainer.visibility = offline
|
||||||
|
binding.animeSourceSettings.visibility = offline
|
||||||
|
binding.animeSourceSearch.visibility = offline
|
||||||
|
binding.animeSourceTitle.visibility = offline
|
||||||
//Source Selection
|
//Source Selection
|
||||||
val source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
var source =
|
||||||
|
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||||
|
setLanguageList(media.selected!!.langIndex, source)
|
||||||
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||||
binding.animeSource.setText(mangaReadSources.names[source])
|
binding.animeSource.setText(mangaReadSources.names[source])
|
||||||
|
|
||||||
mangaReadSources[source].apply {
|
mangaReadSources[source].apply {
|
||||||
binding.animeSourceTitle.text = showUserText
|
binding.animeSourceTitle.text = showUserText
|
||||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, mangaReadSources.names))
|
binding.animeSource.setAdapter(
|
||||||
|
ArrayAdapter(
|
||||||
|
fragment.requireContext(),
|
||||||
|
R.layout.item_dropdown,
|
||||||
|
mangaReadSources.names
|
||||||
|
)
|
||||||
|
)
|
||||||
binding.animeSourceTitle.isSelected = true
|
binding.animeSourceTitle.isSelected = true
|
||||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||||
fragment.onSourceChange(i).apply {
|
fragment.onSourceChange(i).apply {
|
||||||
binding.animeSourceTitle.text = showUserText
|
binding.animeSourceTitle.text = showUserText
|
||||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
|
source = i
|
||||||
|
setLanguageList(0, i)
|
||||||
}
|
}
|
||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
fragment.loadChapters(i)
|
//invalidate if it's the last source
|
||||||
|
val invalidate = i == mangaReadSources.names.size - 1
|
||||||
|
fragment.loadChapters(i, invalidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Subscription
|
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||||
|
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||||
|
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||||
|
ext.sourceLanguage = i
|
||||||
|
fragment.onLangChange(i)
|
||||||
|
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||||
|
binding.animeSourceTitle.text = showUserText
|
||||||
|
showUserTextListener =
|
||||||
|
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
|
setLanguageList(i, source)
|
||||||
|
}
|
||||||
|
subscribeButton(false)
|
||||||
|
fragment.loadChapters(media.selected!!.sourceIndex, true)
|
||||||
|
} ?: run {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//settings
|
||||||
|
binding.animeSourceSettings.setOnClickListener {
|
||||||
|
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||||
|
fragment.openSettings(ext.extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Grids
|
||||||
subscribe = MediaDetailsActivity.PopImageButton(
|
subscribe = MediaDetailsActivity.PopImageButton(
|
||||||
fragment.lifecycleScope,
|
fragment.lifecycleScope,
|
||||||
binding.animeSourceSubscribe,
|
binding.animeSourceSubscribe,
|
||||||
@@ -88,38 +155,156 @@ class MangaReadAdapter(
|
|||||||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
//Icons
|
binding.animeNestedButton.setOnClickListener {
|
||||||
binding.animeSourceGrid.visibility = View.GONE
|
|
||||||
|
val dialogView =
|
||||||
|
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||||
|
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||||
|
var refresh = false
|
||||||
|
var run = false
|
||||||
var reversed = media.selected!!.recyclerReversed
|
var reversed = media.selected!!.recyclerReversed
|
||||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
|
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
|
||||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||||
binding.animeSourceTop.setOnClickListener {
|
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||||
|
dialogBinding.animeSourceTop.setOnClickListener {
|
||||||
reversed = !reversed
|
reversed = !reversed
|
||||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||||
|
run = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Grids
|
||||||
|
dialogBinding.animeSourceGrid.visibility = View.GONE
|
||||||
var selected = when (style) {
|
var selected = when (style) {
|
||||||
0 -> binding.animeSourceList
|
0 -> dialogBinding.animeSourceList
|
||||||
1 -> binding.animeSourceCompact
|
1 -> dialogBinding.animeSourceCompact
|
||||||
else -> binding.animeSourceList
|
else -> dialogBinding.animeSourceList
|
||||||
|
}
|
||||||
|
when (style) {
|
||||||
|
0 -> dialogBinding.layoutText.text = "List"
|
||||||
|
1 -> dialogBinding.layoutText.text = "Compact"
|
||||||
|
else -> dialogBinding.animeSourceList
|
||||||
}
|
}
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
fun selected(it: ImageView) {
|
fun selected(it: ImageButton) {
|
||||||
selected.alpha = 0.33f
|
selected.alpha = 0.33f
|
||||||
selected = it
|
selected = it
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
}
|
}
|
||||||
binding.animeSourceList.setOnClickListener {
|
dialogBinding.animeSourceList.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageButton)
|
||||||
style = 0
|
style = 0
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.layoutText.text = "List"
|
||||||
|
run = true
|
||||||
}
|
}
|
||||||
binding.animeSourceCompact.setOnClickListener {
|
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageButton)
|
||||||
style = 1
|
style = 1
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.layoutText.text = "Compact"
|
||||||
|
run = true
|
||||||
|
}
|
||||||
|
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||||
|
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||||
|
toast("WebView not installed")
|
||||||
|
}
|
||||||
|
//start CookieCatcher activity
|
||||||
|
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||||
|
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
|
||||||
|
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
|
||||||
|
val url = sourceHttp?.baseUrl
|
||||||
|
url?.let {
|
||||||
|
refresh = true
|
||||||
|
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||||
|
.putExtra("url", url)
|
||||||
|
ContextCompat.startActivity(fragment.requireContext(), intent, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Multi download
|
||||||
|
dialogBinding.downloadNo.text = "0"
|
||||||
|
dialogBinding.animeDownloadTop.setOnClickListener {
|
||||||
|
//Alert dialog asking for the number of chapters to download
|
||||||
|
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||||
|
alertDialog.setTitle("Multi Chapter Downloader")
|
||||||
|
alertDialog.setMessage("Enter the number of chapters to download")
|
||||||
|
val input = NumberPicker(currContext())
|
||||||
|
input.minValue = 1
|
||||||
|
input.maxValue = 20
|
||||||
|
input.value = 1
|
||||||
|
alertDialog.setView(input)
|
||||||
|
alertDialog.setPositiveButton("OK") { _, _ ->
|
||||||
|
dialogBinding.downloadNo.text = "${input.value}"
|
||||||
|
}
|
||||||
|
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||||
|
val dialog = alertDialog.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Scanlator
|
||||||
|
dialogBinding.animeScanlatorContainer.visibility =
|
||||||
|
if (options.count() > 1) View.VISIBLE else View.GONE
|
||||||
|
dialogBinding.scanlatorNo.text = "${options.count()}"
|
||||||
|
dialogBinding.animeScanlatorTop.setOnClickListener {
|
||||||
|
val dialogView2 =
|
||||||
|
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||||
|
val checkboxContainer =
|
||||||
|
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||||
|
|
||||||
|
// Dynamically add checkboxes
|
||||||
|
options.forEach { option ->
|
||||||
|
val checkBox = CheckBox(currContext()).apply {
|
||||||
|
text = option
|
||||||
|
}
|
||||||
|
//set checked if it's already selected
|
||||||
|
if (media.selected!!.scanlators != null) {
|
||||||
|
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
|
||||||
|
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||||
|
} else {
|
||||||
|
checkBox.isChecked = true
|
||||||
|
}
|
||||||
|
checkboxContainer.addView(checkBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create AlertDialog
|
||||||
|
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||||
|
.setView(dialogView2)
|
||||||
|
.setPositiveButton("OK") { _, _ ->
|
||||||
|
//add unchecked to hidden
|
||||||
|
hiddenScanlators.clear()
|
||||||
|
for (i in 0 until checkboxContainer.childCount) {
|
||||||
|
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||||
|
if (!checkBox.isChecked) {
|
||||||
|
hiddenScanlators.add(checkBox.text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment.onScanlatorChange(hiddenScanlators)
|
||||||
|
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
}
|
||||||
|
|
||||||
|
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
|
||||||
|
.setTitle("Options")
|
||||||
|
.setView(dialogView)
|
||||||
|
.setPositiveButton("OK") { _, _ ->
|
||||||
|
if (run) fragment.onIconPressed(style, reversed)
|
||||||
|
if (dialogBinding.downloadNo.text != "0") {
|
||||||
|
fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
|
||||||
|
}
|
||||||
|
if (refresh) fragment.loadChapters(source, true)
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { _, _ ->
|
||||||
|
if (refresh) fragment.loadChapters(source, true)
|
||||||
|
}
|
||||||
|
.setOnCancelListener {
|
||||||
|
if (refresh) fragment.loadChapters(source, true)
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
nestedDialog?.show()
|
||||||
|
}
|
||||||
//Chapter Handling
|
//Chapter Handling
|
||||||
handleChapters()
|
handleChapters()
|
||||||
}
|
}
|
||||||
@@ -138,13 +323,40 @@ class MangaReadAdapter(
|
|||||||
for (position in arr.indices) {
|
for (position in arr.indices) {
|
||||||
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
||||||
val chip =
|
val chip =
|
||||||
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root
|
ItemChipBinding.inflate(
|
||||||
|
LayoutInflater.from(fragment.context),
|
||||||
|
binding.animeSourceChipGroup,
|
||||||
|
false
|
||||||
|
).root
|
||||||
chip.isCheckable = true
|
chip.isCheckable = true
|
||||||
fun selected() {
|
fun selected() {
|
||||||
chip.isChecked = true
|
chip.isChecked = true
|
||||||
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
binding.animeWatchChipScroll.smoothScrollTo(
|
||||||
|
(chip.left - screenWidth / 2) + (chip.width / 2),
|
||||||
|
0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
|
||||||
|
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
|
||||||
|
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
|
||||||
|
val startChapterString = if (startChapter != null) {
|
||||||
|
"Ch.$startChapter"
|
||||||
|
} else {
|
||||||
|
names[limit * (position)]
|
||||||
|
}
|
||||||
|
val endChapterString = if (endChapter != null) {
|
||||||
|
"Ch.$endChapter"
|
||||||
|
} else {
|
||||||
|
names[last - 1]
|
||||||
|
}
|
||||||
|
//chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||||
|
chip.text = "$startChapterString - $endChapterString"
|
||||||
|
chip.setTextColor(
|
||||||
|
ContextCompat.getColorStateList(
|
||||||
|
fragment.requireContext(),
|
||||||
|
R.color.chip_text_color
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
selected()
|
selected()
|
||||||
@@ -157,7 +369,14 @@ class MangaReadAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (select != null)
|
if (select != null)
|
||||||
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } }
|
binding.animeWatchChipScroll.apply {
|
||||||
|
post {
|
||||||
|
scrollTo(
|
||||||
|
(select.left - screenWidth / 2) + (select.width / 2),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +386,7 @@ class MangaReadAdapter(
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
fun handleChapters() {
|
fun handleChapters() {
|
||||||
|
|
||||||
val binding = _binding
|
val binding = _binding
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
if (media.manga?.chapters != null) {
|
if (media.manga?.chapters != null) {
|
||||||
@@ -174,7 +394,15 @@ class MangaReadAdapter(
|
|||||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||||
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
|
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
|
||||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||||
if (chapters.contains(continueEp)) {
|
val filteredChapters = chapters.filter { chapterKey ->
|
||||||
|
val chapter = media.manga.chapters!![chapterKey]!!
|
||||||
|
chapter.scanlator !in hiddenScanlators
|
||||||
|
}
|
||||||
|
val formattedChapters = filteredChapters.map {
|
||||||
|
MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
|
||||||
|
}
|
||||||
|
if (formattedChapters.contains(continueEp)) {
|
||||||
|
continueEp = chapters[formattedChapters.indexOf(continueEp)]
|
||||||
binding.animeSourceContinue.visibility = View.VISIBLE
|
binding.animeSourceContinue.visibility = View.VISIBLE
|
||||||
handleProgress(
|
handleProgress(
|
||||||
binding.itemEpisodeProgressCont,
|
binding.itemEpisodeProgressCont,
|
||||||
@@ -220,7 +448,42 @@ class MangaReadAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setLanguageList(lang: Int, source: Int) {
|
||||||
|
val binding = _binding
|
||||||
|
if (mangaReadSources is MangaSources) {
|
||||||
|
val parser = mangaReadSources[source] as? DynamicMangaParser
|
||||||
|
if (parser != null) {
|
||||||
|
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||||
|
ext.sourceLanguage = lang
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||||
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
binding?.animeSourceLanguage?.setText(
|
||||||
|
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val adapter = ArrayAdapter(
|
||||||
|
fragment.requireContext(),
|
||||||
|
R.layout.item_dropdown,
|
||||||
|
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||||
|
)
|
||||||
|
val items = adapter.count
|
||||||
|
binding?.animeSourceLanguageContainer?.visibility =
|
||||||
|
if (items > 1) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
binding?.animeSourceLanguage?.setAdapter(adapter)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScanlatorSelectionListener {
|
||||||
|
fun onScanlatorsSelected()
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,24 @@
|
|||||||
package ani.dantotsu.media.manga
|
package ani.dantotsu.media.manga
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.math.MathUtils.clamp
|
import androidx.core.math.MathUtils.clamp
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@@ -13,27 +26,44 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||||
|
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||||
|
import ani.dantotsu.others.LanguageMapper
|
||||||
|
import ani.dantotsu.parsers.DynamicMangaParser
|
||||||
import ani.dantotsu.parsers.HMangaSources
|
import ani.dantotsu.parsers.HMangaSources
|
||||||
import ani.dantotsu.parsers.MangaParser
|
import ani.dantotsu.parsers.MangaParser
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.parsers.MangaSources
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||||
import ani.dantotsu.subcriptions.Notifications
|
import ani.dantotsu.subcriptions.Notifications
|
||||||
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
|
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
open class MangaReadFragment : Fragment() {
|
open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
private var _binding: FragmentAnimeWatchBinding? = null
|
private var _binding: FragmentAnimeWatchBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private val model: MediaDetailsViewModel by activityViewModels()
|
private val model: MediaDetailsViewModel by activityViewModels()
|
||||||
@@ -48,13 +78,16 @@ open class MangaReadFragment : Fragment() {
|
|||||||
private lateinit var headerAdapter: MangaReadAdapter
|
private lateinit var headerAdapter: MangaReadAdapter
|
||||||
private lateinit var chapterAdapter: MangaChapterAdapter
|
private lateinit var chapterAdapter: MangaChapterAdapter
|
||||||
|
|
||||||
|
val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
var screenWidth = 0f
|
var screenWidth = 0f
|
||||||
private var progress = View.VISIBLE
|
private var progress = View.VISIBLE
|
||||||
|
|
||||||
var continueEp: Boolean = false
|
var continueEp: Boolean = false
|
||||||
var loaded = false
|
var loaded = false
|
||||||
|
|
||||||
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
val uiSettings = loadData("ui_settings", toast = false)
|
||||||
|
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -67,10 +100,23 @@ open class MangaReadFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val intentFilter = IntentFilter().apply {
|
||||||
|
addAction(ACTION_DOWNLOAD_STARTED)
|
||||||
|
addAction(ACTION_DOWNLOAD_FINISHED)
|
||||||
|
addAction(ACTION_DOWNLOAD_FAILED)
|
||||||
|
addAction(ACTION_DOWNLOAD_PROGRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
requireContext(),
|
||||||
|
downloadStatusReceiver,
|
||||||
|
intentFilter,
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
|
||||||
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||||
|
|
||||||
|
|
||||||
var maxGridSize = (screenWidth / 100f).roundToInt()
|
var maxGridSize = (screenWidth / 100f).roundToInt()
|
||||||
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
||||||
|
|
||||||
@@ -107,7 +153,8 @@ open class MangaReadFragment : Fragment() {
|
|||||||
if (media.format == "MANGA" || media.format == "ONE SHOT") {
|
if (media.format == "MANGA" || media.format == "ONE SHOT") {
|
||||||
media.selected = model.loadSelected(media)
|
media.selected = model.loadSelected(media)
|
||||||
|
|
||||||
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
subscribed =
|
||||||
|
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
||||||
|
|
||||||
style = media.selected!!.recyclerStyle
|
style = media.selected!!.recyclerStyle
|
||||||
reverse = media.selected!!.recyclerReversed
|
reverse = media.selected!!.recyclerReversed
|
||||||
@@ -116,9 +163,16 @@ open class MangaReadFragment : Fragment() {
|
|||||||
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
|
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
|
||||||
|
|
||||||
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
|
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
|
||||||
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
headerAdapter.scanlatorSelectionListener = this
|
||||||
|
chapterAdapter =
|
||||||
|
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
||||||
|
|
||||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
|
for (download in downloadManager.mangaDownloadedTypes) {
|
||||||
|
chapterAdapter.stopDownload(download.chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.animeSourceRecycler.adapter =
|
||||||
|
ConcatAdapter(headerAdapter, chapterAdapter)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
model.loadMangaChapters(media, media.selected!!.sourceIndex)
|
model.loadMangaChapters(media, media.selected!!.sourceIndex)
|
||||||
@@ -129,19 +183,59 @@ open class MangaReadFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.animeNotSupported.visibility = View.VISIBLE
|
binding.animeNotSupported.visibility = View.VISIBLE
|
||||||
binding.animeNotSupported.text = getString(R.string.not_supported, media.format ?: "")
|
binding.animeNotSupported.text =
|
||||||
|
getString(R.string.not_supported, media.format ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters ->
|
model.getMangaChapters().observe(viewLifecycleOwner) { _ ->
|
||||||
|
updateChapters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanlatorsSelected() {
|
||||||
|
updateChapters()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun multiDownload(n: Int) {
|
||||||
|
//get last viewed chapter
|
||||||
|
val selected = media.userProgress
|
||||||
|
val chapters = media.manga?.chapters?.values?.toList()
|
||||||
|
//filter by selected language
|
||||||
|
val progressChapterIndex = (chapters?.indexOfFirst {
|
||||||
|
MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||||
|
} ?: 0) + 1
|
||||||
|
|
||||||
|
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
|
||||||
|
|
||||||
|
// Calculate the end index
|
||||||
|
val endIndex = minOf(progressChapterIndex + n, chapters.size)
|
||||||
|
|
||||||
|
//make sure there are enough chapters
|
||||||
|
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
|
||||||
|
|
||||||
|
|
||||||
|
for (chapter in chaptersToDownload) {
|
||||||
|
onMangaChapterDownloadClick(chapter.title!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun updateChapters() {
|
||||||
|
val loadedChapters = model.getMangaChapters().value
|
||||||
if (loadedChapters != null) {
|
if (loadedChapters != null) {
|
||||||
val chapters = loadedChapters[media.selected!!.sourceIndex]
|
val chapters = loadedChapters[media.selected!!.sourceIndex]
|
||||||
if (chapters != null) {
|
if (chapters != null) {
|
||||||
media.manga?.chapters = chapters
|
headerAdapter.options = getScanlators(chapters)
|
||||||
|
val filteredChapters = chapters.filterNot { (_, chapter) ->
|
||||||
|
chapter.scanlator in headerAdapter.hiddenScanlators
|
||||||
|
}
|
||||||
|
|
||||||
|
media.manga?.chapters = filteredChapters.toMutableMap()
|
||||||
|
|
||||||
//CHIP GROUP
|
//CHIP GROUP
|
||||||
val total = chapters.size
|
val total = filteredChapters.size
|
||||||
val divisions = total.toDouble() / 10
|
val divisions = total.toDouble() / 10
|
||||||
start = 0
|
start = 0
|
||||||
end = null
|
end = null
|
||||||
@@ -152,7 +246,7 @@ open class MangaReadFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
headerAdapter.clearChips()
|
headerAdapter.clearChips()
|
||||||
if (total > limit) {
|
if (total > limit) {
|
||||||
val arr = chapters.keys.toTypedArray()
|
val arr = filteredChapters.keys.toTypedArray()
|
||||||
val stored = ceil((total).toDouble() / limit).toInt()
|
val stored = ceil((total).toDouble() / limit).toInt()
|
||||||
val position = clamp(media.selected!!.chip, 0, stored - 1)
|
val position = clamp(media.selected!!.chip, 0, stored - 1)
|
||||||
val last = if (position + 1 == stored) total else (limit * (position + 1))
|
val last = if (position + 1 == stored) total else (limit * (position + 1))
|
||||||
@@ -171,6 +265,16 @@ open class MangaReadFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
|
||||||
|
val scanlators = mutableListOf<String>()
|
||||||
|
if (chap != null) {
|
||||||
|
val chapters = chap.values
|
||||||
|
for (chapter in chapters) {
|
||||||
|
scanlators.add(chapter.scanlator ?: "Unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scanlators.distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSourceChange(i: Int): MangaParser {
|
fun onSourceChange(i: Int): MangaParser {
|
||||||
@@ -185,8 +289,22 @@ open class MangaReadFragment : Fragment() {
|
|||||||
return model.mangaReadSources?.get(i)!!
|
return model.mangaReadSources?.get(i)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadChapters(i: Int) {
|
fun onLangChange(i: Int) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) }
|
val selected = model.loadSelected(media)
|
||||||
|
selected.langIndex = i
|
||||||
|
model.saveSelected(media.id, selected, requireActivity())
|
||||||
|
media.selected = selected
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onScanlatorChange(list: List<String>) {
|
||||||
|
val selected = model.loadSelected(media)
|
||||||
|
selected.scanlators = list
|
||||||
|
model.saveSelected(media.id, selected, requireActivity())
|
||||||
|
media.selected = selected
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadChapters(i: Int, invalidate: Boolean) {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i, invalidate) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onIconPressed(viewType: Int, rev: Boolean) {
|
fun onIconPressed(viewType: Int, rev: Boolean) {
|
||||||
@@ -225,22 +343,220 @@ open class MangaReadFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openSettings(pkg: MangaExtension.Installed) {
|
||||||
|
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||||
|
val activity = activity
|
||||||
|
if (activity is MediaDetailsActivity && isAdded) {
|
||||||
|
val visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||||
|
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||||
|
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||||
|
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||||
|
try {
|
||||||
|
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||||
|
}
|
||||||
|
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||||
|
if (show) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var itemSelected = false
|
||||||
|
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
|
||||||
|
if (allSettings.isNotEmpty()) {
|
||||||
|
var selectedSetting = allSettings[0]
|
||||||
|
if (allSettings.size > 1) {
|
||||||
|
val names =
|
||||||
|
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||||
|
var selectedIndex = 0
|
||||||
|
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||||
|
.setTitle("Select a Source")
|
||||||
|
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||||
|
selectedIndex = which
|
||||||
|
selectedSetting = allSettings[selectedIndex]
|
||||||
|
itemSelected = true
|
||||||
|
dialog.dismiss()
|
||||||
|
|
||||||
|
// Move the fragment transaction here
|
||||||
|
val fragment =
|
||||||
|
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||||
|
changeUIVisibility(true)
|
||||||
|
loadChapters(media.selected!!.sourceIndex, true)
|
||||||
|
}
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||||
|
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
.setOnDismissListener {
|
||||||
|
if (!itemSelected) {
|
||||||
|
changeUIVisibility(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
} else {
|
||||||
|
// If there's only one setting, proceed with the fragment transaction
|
||||||
|
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||||
|
changeUIVisibility(true)
|
||||||
|
loadChapters(media.selected!!.sourceIndex, true)
|
||||||
|
}
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||||
|
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
changeUIVisibility(false)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onMangaChapterClick(i: String) {
|
fun onMangaChapterClick(i: String) {
|
||||||
model.continueMedia = false
|
model.continueMedia = false
|
||||||
media.manga?.chapters?.get(i)?.let {
|
media.manga?.chapters?.get(i)?.let {
|
||||||
media.manga?.selectedChapter = i
|
media.manga?.selectedChapter = i
|
||||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||||
ChapterLoaderDialog.newInstance(it, true).show(requireActivity().supportFragmentManager, "dialog")
|
ChapterLoaderDialog.newInstance(it, true)
|
||||||
|
.show(requireActivity().supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onMangaChapterDownloadClick(i: String) {
|
||||||
|
if (!isNotificationPermissionGranted()) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
requireActivity(),
|
||||||
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model.continueMedia = false
|
||||||
|
media.manga?.chapters?.get(i)?.let { chapter ->
|
||||||
|
val parser =
|
||||||
|
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
|
||||||
|
parser?.let {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val images = parser.imageList("", chapter.sChapter)
|
||||||
|
|
||||||
|
// Create a download task
|
||||||
|
val downloadTask = MangaDownloaderService.DownloadTask(
|
||||||
|
title = media.mainName(),
|
||||||
|
chapter = chapter.title!!,
|
||||||
|
imageData = images,
|
||||||
|
sourceMedia = media,
|
||||||
|
retries = 2,
|
||||||
|
simultaneousDownloads = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
|
||||||
|
|
||||||
|
// If the service is not already running, start it
|
||||||
|
if (!MangaServiceDataSingleton.isServiceRunning) {
|
||||||
|
val intent = Intent(context, MangaDownloaderService::class.java)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
ContextCompat.startForegroundService(requireContext(), intent)
|
||||||
|
}
|
||||||
|
MangaServiceDataSingleton.isServiceRunning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inform the adapter that the download has started
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
chapterAdapter.startDownload(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNotificationPermissionGranted(): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
return ActivityCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onMangaChapterRemoveDownloadClick(i: String) {
|
||||||
|
downloadManager.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
media.mainName(),
|
||||||
|
i,
|
||||||
|
DownloadedType.Type.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
chapterAdapter.deleteDownload(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMangaChapterStopDownloadClick(i: String) {
|
||||||
|
val cancelIntent = Intent().apply {
|
||||||
|
action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD
|
||||||
|
putExtra(MangaDownloaderService.EXTRA_CHAPTER, i)
|
||||||
|
}
|
||||||
|
requireContext().sendBroadcast(cancelIntent)
|
||||||
|
|
||||||
|
// Remove the download from the manager and update the UI
|
||||||
|
downloadManager.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
media.mainName(),
|
||||||
|
i,
|
||||||
|
DownloadedType.Type.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
chapterAdapter.purgeDownload(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (!this@MangaReadFragment::chapterAdapter.isInitialized) return
|
||||||
|
when (intent.action) {
|
||||||
|
ACTION_DOWNLOAD_STARTED -> {
|
||||||
|
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||||
|
chapterNumber?.let { chapterAdapter.startDownload(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_DOWNLOAD_FINISHED -> {
|
||||||
|
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||||
|
chapterNumber?.let { chapterAdapter.stopDownload(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_DOWNLOAD_FAILED -> {
|
||||||
|
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||||
|
chapterNumber?.let {
|
||||||
|
chapterAdapter.purgeDownload(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_DOWNLOAD_PROGRESS -> {
|
||||||
|
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||||
|
val progress = intent.getIntExtra("progress", 0)
|
||||||
|
chapterNumber?.let {
|
||||||
|
chapterAdapter.updateDownloadProgress(it, progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun reload() {
|
private fun reload() {
|
||||||
val selected = model.loadSelected(media)
|
val selected = model.loadSelected(media)
|
||||||
|
|
||||||
//Find latest chapter for subscription
|
//Find latest chapter for subscription
|
||||||
selected.latest = media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
selected.latest =
|
||||||
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
||||||
|
selected.latest =
|
||||||
|
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||||
|
|
||||||
model.saveSelected(media.id, selected, requireActivity())
|
model.saveSelected(media.id, selected, requireActivity())
|
||||||
headerAdapter.handleChapters()
|
headerAdapter.handleChapters()
|
||||||
@@ -249,7 +565,8 @@ open class MangaReadFragment : Fragment() {
|
|||||||
if (media.manga!!.chapters != null) {
|
if (media.manga!!.chapters != null) {
|
||||||
val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null
|
val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null
|
||||||
arr.addAll(
|
arr.addAll(
|
||||||
media.manga!!.chapters!!.values.toList().slice(start..(end ?: (media.manga!!.chapters!!.size - 1)))
|
media.manga!!.chapters!!.values.toList()
|
||||||
|
.slice(start..(end ?: (media.manga!!.chapters!!.size - 1)))
|
||||||
)
|
)
|
||||||
if (reverse)
|
if (reverse)
|
||||||
arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr
|
arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr
|
||||||
@@ -262,6 +579,7 @@ open class MangaReadFragment : Fragment() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
model.mangaReadSources?.flushText()
|
model.mangaReadSources?.flushText()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var state: Parcelable? = null
|
private var state: Parcelable? = null
|
||||||
@@ -275,4 +593,12 @@ open class MangaReadFragment : Fragment() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
|
||||||
|
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
|
||||||
|
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
|
||||||
|
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
|
||||||
|
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.net.Uri
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -13,8 +14,8 @@ import androidx.core.view.updateLayoutParams
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
import ani.dantotsu.media.manga.MangaChapter
|
import ani.dantotsu.media.manga.MangaChapter
|
||||||
import ani.dantotsu.parsers.DynamicMangaParser
|
|
||||||
import ani.dantotsu.settings.CurrentReaderSettings
|
import ani.dantotsu.settings.CurrentReaderSettings
|
||||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@@ -22,13 +23,11 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
abstract class BaseImageAdapter(
|
abstract class BaseImageAdapter(
|
||||||
val activity: MangaReaderActivity,
|
val activity: MangaReaderActivity,
|
||||||
@@ -90,7 +89,8 @@ abstract class BaseImageAdapter(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val detector = GestureDetectorCompat(view.context, object : GesturesListener() {
|
val detector = GestureDetectorCompat(view.context, object : GesturesListener() {
|
||||||
override fun onSingleClick(event: MotionEvent) = activity.handleController()
|
override fun onSingleClick(event: MotionEvent) =
|
||||||
|
activity.handleController(event = event)
|
||||||
})
|
})
|
||||||
view.findViewById<View>(R.id.imgProgCover).apply {
|
view.findViewById<View>(R.id.imgProgCover).apply {
|
||||||
setOnTouchListener { _, event ->
|
setOnTouchListener { _, event ->
|
||||||
@@ -113,13 +113,19 @@ abstract class BaseImageAdapter(
|
|||||||
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) }
|
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract fun isZoomed(): Boolean
|
||||||
|
abstract fun setZoom(zoom: Float)
|
||||||
|
|
||||||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/*suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
suspend fun Context.loadBitmap_old(
|
||||||
|
link: FileUrl,
|
||||||
|
transforms: List<BitmapTransformation>
|
||||||
|
): Bitmap? { //still used in some places
|
||||||
return tryWithSuspend {
|
return tryWithSuspend {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Glide.with(this@loadBitmap)
|
Glide.with(this@loadBitmap_old)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.let {
|
.let {
|
||||||
if (link.url.startsWith("file://")) {
|
if (link.url.startsWith("file://")) {
|
||||||
@@ -133,8 +139,7 @@ abstract class BaseImageAdapter(
|
|||||||
.let {
|
.let {
|
||||||
if (transforms.isNotEmpty()) {
|
if (transforms.isNotEmpty()) {
|
||||||
it.transform(*transforms.toTypedArray())
|
it.transform(*transforms.toTypedArray())
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,26 +147,31 @@ abstract class BaseImageAdapter(
|
|||||||
.get()
|
.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
|
||||||
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
suspend fun Context.loadBitmap(
|
||||||
|
link: FileUrl,
|
||||||
|
transforms: List<BitmapTransformation>
|
||||||
|
): Bitmap? {
|
||||||
return tryWithSuspend {
|
return tryWithSuspend {
|
||||||
val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>()
|
val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>()
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Glide.with(this@loadBitmap)
|
Glide.with(this@loadBitmap)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.let {
|
.let {
|
||||||
if (link.url.startsWith("file://")) {
|
val fileUri = Uri.fromFile(File(link.url)).toString()
|
||||||
it.load(link.url)
|
val localFile = File(link.url)
|
||||||
|
if (localFile.exists()) {
|
||||||
|
it.load(localFile.absoluteFile)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
} else {
|
} else {
|
||||||
println("bitmap from cache")
|
|
||||||
println(link.url)
|
|
||||||
println(mangaCache.get(link.url))
|
|
||||||
println("cache size: ${mangaCache.size()}")
|
|
||||||
mangaCache.get(link.url)?.let { imageData ->
|
mangaCache.get(link.url)?.let { imageData ->
|
||||||
val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap)
|
val bitmap = imageData.fetchAndProcessImage(
|
||||||
|
imageData.page,
|
||||||
|
imageData.source,
|
||||||
|
context = this@loadBitmap
|
||||||
|
)
|
||||||
it.load(bitmap)
|
it.load(bitmap)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package ani.dantotsu.media.manga.mangareader
|
package ani.dantotsu.media.manga.mangareader
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -12,9 +14,9 @@ import ani.dantotsu.BottomSheetDialogFragment
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.currActivity
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||||
import ani.dantotsu.media.manga.MangaChapter
|
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.media.MediaSingleton
|
import ani.dantotsu.media.MediaSingleton
|
||||||
|
import ani.dantotsu.media.manga.MangaChapter
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.tryWith
|
import ani.dantotsu.tryWith
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -45,13 +47,21 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
|||||||
loaded = true
|
loaded = true
|
||||||
binding.selectorAutoText.text = chp.title
|
binding.selectorAutoText.text = chp.title
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if(model.loadMangaChapterImages(chp, m.selected!!)) {
|
if (model.loadMangaChapterImages(
|
||||||
|
chp,
|
||||||
|
m.selected!!,
|
||||||
|
m.mainName()
|
||||||
|
)
|
||||||
|
) {
|
||||||
val activity = currActivity()
|
val activity = currActivity()
|
||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
tryWith { dismiss() }
|
tryWith { dismiss() }
|
||||||
if (launch) {
|
if (launch) {
|
||||||
MediaSingleton.media = m
|
MediaSingleton.media = m
|
||||||
val intent = Intent(activity, MangaReaderActivity::class.java)//.apply { putExtra("media", m) }
|
val intent = Intent(
|
||||||
|
activity,
|
||||||
|
MangaReaderActivity::class.java
|
||||||
|
)//.apply { putExtra("media", m) }
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,8 +71,18 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
||||||
|
val window = dialog?.window
|
||||||
|
window?.statusBarColor = Color.TRANSPARENT
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
val theme = requireContext().theme
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||||
|
window?.navigationBarColor = typedValue.data
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ open class ImageAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadImage(position: Int, parent: View): Boolean {
|
override suspend fun loadImage(position: Int, parent: View): Boolean {
|
||||||
val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures) ?: return false
|
val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
|
||||||
|
?: return false
|
||||||
val progress = parent.findViewById<View>(R.id.imgProgProgress) ?: return false
|
val progress = parent.findViewById<View>(R.id.imgProgProgress) ?: return false
|
||||||
imageView.recycle()
|
imageView.recycle()
|
||||||
imageView.visibility = View.GONE
|
imageView.visibility = View.GONE
|
||||||
@@ -60,10 +61,12 @@ open class ImageAdapter(
|
|||||||
if (settings.layout != PAGED)
|
if (settings.layout != PAGED)
|
||||||
parent.updateLayoutParams {
|
parent.updateLayoutParams {
|
||||||
if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) {
|
if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) {
|
||||||
sHeight = if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt()
|
sHeight =
|
||||||
|
if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt()
|
||||||
height = sHeight
|
height = sHeight
|
||||||
} else {
|
} else {
|
||||||
sWidth = if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt()
|
sWidth =
|
||||||
|
if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt()
|
||||||
width = sWidth
|
width = sWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +76,8 @@ open class ImageAdapter(
|
|||||||
|
|
||||||
val parentArea = sWidth * sHeight * 1f
|
val parentArea = sWidth * sHeight * 1f
|
||||||
val bitmapArea = bitmap.width * bitmap.height * 1f
|
val bitmapArea = bitmap.width * bitmap.height * 1f
|
||||||
val scale = if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea)
|
val scale =
|
||||||
|
if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea)
|
||||||
|
|
||||||
imageView.maxScale = scale * 1.1f
|
imageView.maxScale = scale * 1.1f
|
||||||
imageView.minScale = scale
|
imageView.minScale = scale
|
||||||
@@ -87,4 +91,16 @@ open class ImageAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = images.size
|
override fun getItemCount(): Int = images.size
|
||||||
|
|
||||||
|
override fun isZoomed(): Boolean {
|
||||||
|
val imageView =
|
||||||
|
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
|
||||||
|
return imageView.scale > imageView.minScale
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setZoom(zoom: Float) {
|
||||||
|
val imageView =
|
||||||
|
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
|
||||||
|
imageView.setScaleAndCenter(zoom, imageView.center)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package ani.dantotsu.media.manga.mangareader
|
|||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -11,6 +14,7 @@ import android.view.*
|
|||||||
import android.view.KeyEvent.*
|
import android.view.KeyEvent.*
|
||||||
import android.view.animation.OvershootInterpolator
|
import android.view.animation.OvershootInterpolator
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
|
import android.widget.CheckBox
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@@ -25,6 +29,8 @@ import androidx.viewpager2.widget.ViewPager2
|
|||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.discord.Discord
|
import ani.dantotsu.connections.discord.Discord
|
||||||
|
import ani.dantotsu.connections.discord.DiscordService
|
||||||
|
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
||||||
import ani.dantotsu.connections.discord.RPC
|
import ani.dantotsu.connections.discord.RPC
|
||||||
import ani.dantotsu.connections.updateProgress
|
import ani.dantotsu.connections.updateProgress
|
||||||
import ani.dantotsu.databinding.ActivityMangaReaderBinding
|
import ani.dantotsu.databinding.ActivityMangaReaderBinding
|
||||||
@@ -35,7 +41,7 @@ import ani.dantotsu.media.manga.MangaCache
|
|||||||
import ani.dantotsu.media.manga.MangaChapter
|
import ani.dantotsu.media.manga.MangaChapter
|
||||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.LangSet
|
||||||
import ani.dantotsu.parsers.HMangaSources
|
import ani.dantotsu.parsers.HMangaSources
|
||||||
import ani.dantotsu.parsers.MangaImage
|
import ani.dantotsu.parsers.MangaImage
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.parsers.MangaSources
|
||||||
@@ -45,6 +51,7 @@ import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.*
|
|||||||
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
|
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
|
||||||
import ani.dantotsu.settings.ReaderSettings
|
import ani.dantotsu.settings.ReaderSettings
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
@@ -52,8 +59,6 @@ import com.google.firebase.crashlytics.ktx.crashlytics
|
|||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -78,7 +83,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private var isContVisible = false
|
private var isContVisible = false
|
||||||
private var showProgressDialog = true
|
private var showProgressDialog = true
|
||||||
private var progressDialog: AlertDialog.Builder? = null
|
|
||||||
|
//private var progressDialog: AlertDialog.Builder? = null
|
||||||
private var maxChapterPage = 0L
|
private var maxChapterPage = 0L
|
||||||
private var currentChapterPage = 0L
|
private var currentChapterPage = 0L
|
||||||
|
|
||||||
@@ -99,7 +105,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
||||||
if (displayCutout != null) {
|
if (displayCutout != null) {
|
||||||
if (displayCutout.boundingRects.size > 0) {
|
if (displayCutout.boundingRects.size > 0) {
|
||||||
notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height())
|
notchHeight = min(
|
||||||
|
displayCutout.boundingRects[0].width(),
|
||||||
|
displayCutout.boundingRects[0].height()
|
||||||
|
)
|
||||||
checkNotch()
|
checkNotch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,12 +128,18 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
mangaCache.clear()
|
mangaCache.clear()
|
||||||
rpc?.close()
|
if (DiscordServiceRunningSingleton.running) {
|
||||||
|
DiscordServiceRunningSingleton.running = false
|
||||||
|
val stopIntent = Intent(this, DiscordService::class.java)
|
||||||
|
stopService(stopIntent)
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
LangSet.setLocale(this)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
|
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
@@ -136,8 +151,14 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
progress { finish() }
|
progress { finish() }
|
||||||
}
|
}
|
||||||
|
|
||||||
settings = loadData("reader_settings", this) ?: ReaderSettings().apply { saveData("reader_settings", this) }
|
settings = loadData("reader_settings", this)
|
||||||
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
?: ReaderSettings().apply { saveData("reader_settings", this) }
|
||||||
|
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply {
|
||||||
|
saveData(
|
||||||
|
"ui_settings",
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
controllerDuration = (uiSettings.animationSpeed * 200).toLong()
|
controllerDuration = (uiSettings.animationSpeed * 200).toLong()
|
||||||
|
|
||||||
hideBars()
|
hideBars()
|
||||||
@@ -162,9 +183,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
sliding = true
|
sliding = true
|
||||||
if (settings.default.layout != PAGED)
|
if (settings.default.layout != PAGED)
|
||||||
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 } ?: 1))
|
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
|
||||||
|
?: 1))
|
||||||
else
|
else
|
||||||
binding.mangaReaderPager.currentItem = (value.toInt() - 1) / (dualPage { 2 } ?: 1)
|
binding.mangaReaderPager.currentItem =
|
||||||
|
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
|
||||||
pageSliderHide()
|
pageSliderHide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +220,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
val mangaSources = MangaSources
|
val mangaSources = MangaSources
|
||||||
val scope = lifecycleScope
|
val scope = lifecycleScope
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow, this@MangaReaderActivity)
|
||||||
}
|
}
|
||||||
model.mangaReadSources = mangaSources
|
model.mangaReadSources = mangaSources
|
||||||
} else {
|
} else {
|
||||||
@@ -213,7 +236,12 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex]
|
//check that index is not out of bounds (crash fix)
|
||||||
|
if (media.selected!!.sourceIndex >= model.mangaReadSources!!.names.size) {
|
||||||
|
media.selected!!.sourceIndex = 0
|
||||||
|
}
|
||||||
|
binding.mangaReaderSource.text =
|
||||||
|
model.mangaReadSources!!.names[media.selected!!.sourceIndex]
|
||||||
|
|
||||||
binding.mangaReaderTitle.text = media.userPreferredName
|
binding.mangaReaderTitle.text = media.userPreferredName
|
||||||
|
|
||||||
@@ -226,34 +254,30 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}")
|
chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}")
|
||||||
}
|
}
|
||||||
|
|
||||||
showProgressDialog = if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false
|
showProgressDialog =
|
||||||
progressDialog =
|
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
|
||||||
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
|
?: true else false
|
||||||
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.title_update_progress)).apply {
|
|
||||||
setMultiChoiceItems(
|
|
||||||
arrayOf(getString(R.string.dont_ask_again, media.userPreferredName)),
|
|
||||||
booleanArrayOf(false)
|
|
||||||
) { _, _, isChecked ->
|
|
||||||
if (isChecked) progressDialog = null
|
|
||||||
saveData("${media.id}_progressDialog", isChecked)
|
|
||||||
showProgressDialog = isChecked
|
|
||||||
}
|
|
||||||
setOnCancelListener { hideBars() }
|
|
||||||
}
|
|
||||||
else null
|
|
||||||
|
|
||||||
//Chapter Change
|
//Chapter Change
|
||||||
fun change(index: Int) {
|
fun change(index: Int) {
|
||||||
mangaCache.clear()
|
mangaCache.clear()
|
||||||
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
|
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
|
||||||
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog")
|
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!)
|
||||||
|
.show(supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
//ChapterSelector
|
//ChapterSelector
|
||||||
binding.mangaReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr)
|
binding.mangaReaderChapterSelect.adapter =
|
||||||
|
NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr)
|
||||||
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
||||||
binding.mangaReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
binding.mangaReaderChapterSelect.onItemSelectedListener =
|
||||||
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
|
object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(
|
||||||
|
p0: AdapterView<*>?,
|
||||||
|
p1: View?,
|
||||||
|
position: Int,
|
||||||
|
p3: Long
|
||||||
|
) {
|
||||||
if (position != currentChapterIndex) change(position)
|
if (position != currentChapterIndex) change(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,27 +313,57 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
saveData("${media.id}_current_chp", chap.number, this)
|
saveData("${media.id}_current_chp", chap.number, this)
|
||||||
currentChapterIndex = chaptersArr.indexOf(chap.number)
|
currentChapterIndex = chaptersArr.indexOf(chap.number)
|
||||||
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
||||||
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
binding.mangaReaderNextChap.text =
|
||||||
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||||
|
binding.mangaReaderPrevChap.text =
|
||||||
|
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||||
applySettings()
|
applySettings()
|
||||||
rpc?.close()
|
val context = this
|
||||||
rpc = Discord.defaultRPC()
|
val incognito = context.getSharedPreferences("Dantotsu", 0)
|
||||||
rpc?.send {
|
?.getBoolean("incognito", false) ?: false
|
||||||
type = RPC.Type.WATCHING
|
if (isOnline(context) && Discord.token != null && !incognito) {
|
||||||
activityName = media.userPreferredName
|
lifecycleScope.launch {
|
||||||
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number)
|
val presence = RPC.createPresence(
|
||||||
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}"
|
RPC.Companion.RPCData(
|
||||||
media.cover?.let { cover ->
|
applicationId = Discord.application_Id,
|
||||||
largeImage = RPC.Link(media.userPreferredName, cover)
|
type = RPC.Type.WATCHING,
|
||||||
|
activityName = media.userPreferredName,
|
||||||
|
details = chap.title?.takeIf { it.isNotEmpty() }
|
||||||
|
?: getString(R.string.chapter_num, chap.number),
|
||||||
|
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
|
||||||
|
largeImage = media.cover?.let { cover ->
|
||||||
|
RPC.Link(media.userPreferredName, cover)
|
||||||
|
},
|
||||||
|
smallImage = RPC.Link(
|
||||||
|
"Dantotsu",
|
||||||
|
Discord.small_Image
|
||||||
|
),
|
||||||
|
buttons = mutableListOf(
|
||||||
|
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||||
|
RPC.Link(
|
||||||
|
"Stream on Dantotsu",
|
||||||
|
"https://github.com/rebelonion/Dantotsu/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val intent = Intent(context, DiscordService::class.java).apply {
|
||||||
|
putExtra("presence", presence)
|
||||||
}
|
}
|
||||||
media.shareLink?.let { link ->
|
DiscordServiceRunningSingleton.running = true
|
||||||
buttons.add(0, RPC.Link(getString(R.string.view_manga), link))
|
startService(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!) }
|
scope.launch(Dispatchers.IO) {
|
||||||
|
model.loadMangaChapterImages(
|
||||||
|
chapter,
|
||||||
|
media.selected!!,
|
||||||
|
media.mainName()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
private val snapHelper = PagerSnapHelper()
|
||||||
@@ -322,6 +376,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke()
|
if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke()
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
Force -> callback.invoke()
|
Force -> callback.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,7 +408,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
maxChapterPage = chapImages.size.toLong()
|
maxChapterPage = chapImages.size.toLong()
|
||||||
saveData("${media.id}_${chapter.number}_max", maxChapterPage)
|
saveData("${media.id}_${chapter.number}_max", maxChapterPage)
|
||||||
|
|
||||||
imageAdapter = dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter)
|
imageAdapter =
|
||||||
|
dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter)
|
||||||
|
|
||||||
if (chapImages.size > 1) {
|
if (chapImages.size > 1) {
|
||||||
binding.mangaReaderSlider.apply {
|
binding.mangaReaderSlider.apply {
|
||||||
@@ -374,8 +430,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) {
|
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) {
|
||||||
binding.mangaReaderSwipy.vertical = true
|
binding.mangaReaderSwipy.vertical = true
|
||||||
if (settings.default.direction == TOP_TO_BOTTOM) {
|
if (settings.default.direction == TOP_TO_BOTTOM) {
|
||||||
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
|
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
|
?: getString(R.string.no_chapter)
|
||||||
|
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||||
|
?: getString(R.string.no_chapter)
|
||||||
binding.mangaReaderSwipy.onTopSwiped = {
|
binding.mangaReaderSwipy.onTopSwiped = {
|
||||||
binding.mangaReaderPreviousChapter.performClick()
|
binding.mangaReaderPreviousChapter.performClick()
|
||||||
}
|
}
|
||||||
@@ -383,8 +441,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
binding.mangaReaderNextChapter.performClick()
|
binding.mangaReaderNextChapter.performClick()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
|
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
|
?: getString(R.string.no_chapter)
|
||||||
|
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||||
|
?: getString(R.string.no_chapter)
|
||||||
binding.mangaReaderSwipy.onTopSwiped = {
|
binding.mangaReaderSwipy.onTopSwiped = {
|
||||||
binding.mangaReaderNextChapter.performClick()
|
binding.mangaReaderNextChapter.performClick()
|
||||||
}
|
}
|
||||||
@@ -407,8 +467,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
binding.mangaReaderSwipy.vertical = false
|
binding.mangaReaderSwipy.vertical = false
|
||||||
if (settings.default.direction == RIGHT_TO_LEFT) {
|
if (settings.default.direction == RIGHT_TO_LEFT) {
|
||||||
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
|
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
|
?: getString(R.string.no_chapter)
|
||||||
|
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||||
|
?: getString(R.string.no_chapter)
|
||||||
binding.mangaReaderSwipy.onLeftSwiped = {
|
binding.mangaReaderSwipy.onLeftSwiped = {
|
||||||
binding.mangaReaderNextChapter.performClick()
|
binding.mangaReaderNextChapter.performClick()
|
||||||
}
|
}
|
||||||
@@ -416,8 +478,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
binding.mangaReaderPreviousChapter.performClick()
|
binding.mangaReaderPreviousChapter.performClick()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
|
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
|
?: getString(R.string.no_chapter)
|
||||||
|
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||||
|
?: getString(R.string.no_chapter)
|
||||||
binding.mangaReaderSwipy.onLeftSwiped = {
|
binding.mangaReaderSwipy.onLeftSwiped = {
|
||||||
binding.mangaReaderPreviousChapter.performClick()
|
binding.mangaReaderPreviousChapter.performClick()
|
||||||
}
|
}
|
||||||
@@ -442,7 +506,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
if (settings.default.layout != PAGED) {
|
if (settings.default.layout != PAGED) {
|
||||||
|
|
||||||
binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE
|
binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE
|
||||||
binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled = settings.default.rotation
|
binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled =
|
||||||
|
settings.default.rotation
|
||||||
|
|
||||||
val detector = GestureDetectorCompat(this, object : GesturesListener() {
|
val detector = GestureDetectorCompat(this, object : GesturesListener() {
|
||||||
override fun onLongPress(e: MotionEvent) {
|
override fun onLongPress(e: MotionEvent) {
|
||||||
@@ -450,18 +515,31 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
child ?: return@let false
|
child ?: return@let false
|
||||||
val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child)
|
val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child)
|
||||||
val callback: (ImageViewDialog) -> Unit = { dialog ->
|
val callback: (ImageViewDialog) -> Unit = { dialog ->
|
||||||
lifecycleScope.launch { imageAdapter?.loadImage(pos, child as GestureFrameLayout) }
|
lifecycleScope.launch {
|
||||||
binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
imageAdapter?.loadImage(
|
||||||
|
pos,
|
||||||
|
child as GestureFrameLayout
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.mangaReaderRecycler.performHapticFeedback(
|
||||||
|
HapticFeedbackConstants.LONG_PRESS
|
||||||
|
)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
dualPage {
|
dualPage {
|
||||||
val page = chapter.dualPages().getOrNull(pos) ?: return@dualPage false
|
val page =
|
||||||
|
chapter.dualPages().getOrNull(pos) ?: return@dualPage false
|
||||||
val nextPage = page.second
|
val nextPage = page.second
|
||||||
if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null)
|
if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null)
|
||||||
onImageLongClicked(pos * 2, nextPage, page.first, callback)
|
onImageLongClicked(pos * 2, nextPage, page.first, callback)
|
||||||
else
|
else
|
||||||
onImageLongClicked(pos * 2, page.first, nextPage, callback)
|
onImageLongClicked(pos * 2, page.first, nextPage, callback)
|
||||||
} ?: onImageLongClicked(pos, chapImages.getOrNull(pos) ?: return@let false, null, callback)
|
} ?: onImageLongClicked(
|
||||||
|
pos,
|
||||||
|
chapImages.getOrNull(pos) ?: return@let false,
|
||||||
|
null,
|
||||||
|
callback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
super.onLongPress(e)
|
super.onLongPress(e)
|
||||||
@@ -503,12 +581,16 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
&& (!v.canScrollVertically(-1) || !v.canScrollVertically(1)))
|
&& (!v.canScrollVertically(-1) || !v.canScrollVertically(1)))
|
||||||
||
|
||
|
||||||
((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT)
|
((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT)
|
||||||
&& (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(1)))
|
&& (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(
|
||||||
|
1
|
||||||
|
)))
|
||||||
) {
|
) {
|
||||||
handleController(true)
|
handleController(true)
|
||||||
} else handleController(false)
|
} else handleController(false)
|
||||||
}
|
}
|
||||||
updatePageNumber(manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 } ?: 1) + 1)
|
updatePageNumber(
|
||||||
|
manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 }
|
||||||
|
?: 1) + 1)
|
||||||
super.onScrolled(v, dx, dy)
|
super.onScrolled(v, dx, dy)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -579,6 +661,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> {
|
KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> {
|
||||||
if (event.keyCode == KEYCODE_VOLUME_DOWN)
|
if (event.keyCode == KEYCODE_VOLUME_DOWN)
|
||||||
if (!settings.default.volumeButtons)
|
if (!settings.default.volumeButtons)
|
||||||
@@ -588,6 +671,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
super.dispatchKeyEvent(event)
|
super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
@@ -620,8 +704,60 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
goneTimer.schedule(timerTask, controllerDuration)
|
goneTimer.schedule(timerTask, controllerDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleController(shouldShow: Boolean? = null) {
|
enum class pressPos {
|
||||||
|
LEFT, RIGHT, CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
|
||||||
|
var pressLocation = pressPos.CENTER
|
||||||
if (!sliding) {
|
if (!sliding) {
|
||||||
|
if (event != null && settings.default.layout == PAGED) {
|
||||||
|
if (event.action != MotionEvent.ACTION_UP) return
|
||||||
|
val x = event.rawX.toInt()
|
||||||
|
val y = event.rawY.toInt()
|
||||||
|
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
|
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
|
||||||
|
if (screenWidth / 5 in (x + 1)..<y) {
|
||||||
|
pressLocation = if (settings.default.direction == RIGHT_TO_LEFT) {
|
||||||
|
pressPos.RIGHT
|
||||||
|
} else {
|
||||||
|
pressPos.LEFT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
|
||||||
|
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
|
||||||
|
pressLocation = if (settings.default.direction == RIGHT_TO_LEFT) {
|
||||||
|
pressPos.LEFT
|
||||||
|
} else {
|
||||||
|
pressPos.RIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if pressLocation is left or right go to previous or next page (paged mode only)
|
||||||
|
if (pressLocation == pressPos.LEFT) {
|
||||||
|
|
||||||
|
if (binding.mangaReaderPager.currentItem > 0) {
|
||||||
|
//if the current images zoomed in, go back to normal before going to previous page
|
||||||
|
if (imageAdapter?.isZoomed() == true) {
|
||||||
|
imageAdapter?.setZoom(1f)
|
||||||
|
}
|
||||||
|
binding.mangaReaderPager.currentItem -= 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (pressLocation == pressPos.RIGHT) {
|
||||||
|
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
|
||||||
|
//if the current images zoomed in, go back to normal before going to next page
|
||||||
|
if (imageAdapter?.isZoomed() == true) {
|
||||||
|
imageAdapter?.setZoom(1f)
|
||||||
|
}
|
||||||
|
//if right to left, go to previous page
|
||||||
|
binding.mangaReaderPager.currentItem += 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!settings.showSystemBars) {
|
if (!settings.showSystemBars) {
|
||||||
hideBars()
|
hideBars()
|
||||||
checkNotch()
|
checkNotch()
|
||||||
@@ -662,8 +798,14 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
isContVisible = false
|
isContVisible = false
|
||||||
if (!isAnimating) {
|
if (!isAnimating) {
|
||||||
isAnimating = true
|
isAnimating = true
|
||||||
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start()
|
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f)
|
||||||
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f)
|
.setDuration(controllerDuration).start()
|
||||||
|
ObjectAnimator.ofFloat(
|
||||||
|
binding.mangaReaderBottomLayout,
|
||||||
|
"translationY",
|
||||||
|
0f,
|
||||||
|
128f
|
||||||
|
)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
|
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
@@ -672,7 +814,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
isContVisible = true
|
isContVisible = true
|
||||||
binding.mangaReaderCont.visibility = View.VISIBLE
|
binding.mangaReaderCont.visibility = View.VISIBLE
|
||||||
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start()
|
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f)
|
||||||
|
.setDuration(controllerDuration).start()
|
||||||
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f)
|
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f)
|
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f)
|
||||||
@@ -698,6 +841,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
model.loadMangaChapterImages(
|
model.loadMangaChapterImages(
|
||||||
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
||||||
media.selected!!,
|
media.selected!!,
|
||||||
|
media.mainName(),
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
loading = false
|
loading = false
|
||||||
@@ -706,23 +850,55 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun progress(runnable: Runnable) {
|
private fun progress(runnable: Runnable) {
|
||||||
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
|
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
|
||||||
if (showProgressDialog && progressDialog != null) {
|
showProgressDialog =
|
||||||
progressDialog?.setCancelable(false)
|
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
|
||||||
?.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
?: true else false
|
||||||
|
if (showProgressDialog) {
|
||||||
|
|
||||||
|
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
|
||||||
|
val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox)
|
||||||
|
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
|
||||||
|
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
saveData("${media.id}_progressDialog", !isChecked)
|
||||||
|
showProgressDialog = !isChecked
|
||||||
|
}
|
||||||
|
val incognito =
|
||||||
|
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
?.getBoolean("incognito", false) ?: false
|
||||||
|
AlertDialog.Builder(this, R.style.MyPopup)
|
||||||
|
.setTitle(getString(R.string.title_update_progress))
|
||||||
|
.apply {
|
||||||
|
if (incognito) {
|
||||||
|
setMessage(getString(R.string.incognito_will_not_update))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setView(dialogView)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
||||||
saveData("${media.id}_save_progress", true)
|
saveData("${media.id}_save_progress", true)
|
||||||
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString())
|
updateProgress(
|
||||||
|
media,
|
||||||
|
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
runnable.run()
|
runnable.run()
|
||||||
}
|
}
|
||||||
?.setNegativeButton(getString(R.string.no)) { dialog, _ ->
|
.setNegativeButton(getString(R.string.no)) { dialog, _ ->
|
||||||
saveData("${media.id}_save_progress", false)
|
saveData("${media.id}_save_progress", false)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
runnable.run()
|
runnable.run()
|
||||||
}
|
}
|
||||||
progressDialog?.show()
|
.setOnCancelListener { hideBars() }
|
||||||
|
.create()
|
||||||
|
.show()
|
||||||
} else {
|
} else {
|
||||||
if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true)
|
if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true)
|
||||||
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString())
|
updateProgress(
|
||||||
|
media,
|
||||||
|
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
runnable.run()
|
runnable.run()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import kotlin.math.max
|
|||||||
|
|
||||||
class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) :
|
class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) :
|
||||||
LinearLayoutManager(context, orientation, reverseLayout) {
|
LinearLayoutManager(context, orientation, reverseLayout) {
|
||||||
private val mOrientationHelper: OrientationHelper = OrientationHelper.createOrientationHelper(this, orientation)
|
private val mOrientationHelper: OrientationHelper =
|
||||||
|
OrientationHelper.createOrientationHelper(this, orientation)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us,
|
* As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us,
|
||||||
@@ -37,7 +38,8 @@ class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayo
|
|||||||
val currentPosition: Int = getPosition(child ?: return) + layoutDirection
|
val currentPosition: Int = getPosition(child ?: return) + layoutDirection
|
||||||
|
|
||||||
if (layoutDirection == 1) {
|
if (layoutDirection == 1) {
|
||||||
val scrollingOffset = (mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding)
|
val scrollingOffset =
|
||||||
|
(mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding)
|
||||||
((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach {
|
((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach {
|
||||||
if (it >= 0 && it < state.itemCount) {
|
if (it >= 0 && it < state.itemCount) {
|
||||||
layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset))
|
layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset))
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||||||
private var _binding: BottomSheetCurrentReaderSettingsBinding? = null
|
private var _binding: BottomSheetCurrentReaderSettingsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetCurrentReaderSettingsBinding.inflate(inflater, container, false)
|
_binding = BottomSheetCurrentReaderSettingsBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -24,11 +28,14 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||||||
val activity = requireActivity() as MangaReaderActivity
|
val activity = requireActivity() as MangaReaderActivity
|
||||||
val settings = activity.settings.default
|
val settings = activity.settings.default
|
||||||
|
|
||||||
binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
|
binding.readerDirectionText.text =
|
||||||
|
resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
|
||||||
binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
|
binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
|
||||||
binding.readerDirection.setOnClickListener {
|
binding.readerDirection.setOnClickListener {
|
||||||
settings.direction = Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM
|
settings.direction =
|
||||||
binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
|
Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM
|
||||||
|
binding.readerDirectionText.text =
|
||||||
|
resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
|
||||||
binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
|
binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
|
||||||
activity.applySettings()
|
activity.applySettings()
|
||||||
}
|
}
|
||||||
@@ -56,7 +63,8 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||||||
activity.applySettings()
|
activity.applySettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
|
binding.readerLayoutText.text =
|
||||||
|
resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
|
||||||
var selected = list[settings.layout.ordinal]
|
var selected = list[settings.layout.ordinal]
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
|
|
||||||
@@ -65,8 +73,10 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||||||
selected.alpha = 0.33f
|
selected.alpha = 0.33f
|
||||||
selected = imageButton
|
selected = imageButton
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
settings.layout = CurrentReaderSettings.Layouts[index]?:CurrentReaderSettings.Layouts.CONTINUOUS
|
settings.layout =
|
||||||
binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
|
CurrentReaderSettings.Layouts[index] ?: CurrentReaderSettings.Layouts.CONTINUOUS
|
||||||
|
binding.readerLayoutText.text =
|
||||||
|
resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
|
||||||
activity.applySettings()
|
activity.applySettings()
|
||||||
paddingAvailable(settings.layout.ordinal != 0)
|
paddingAvailable(settings.layout.ordinal != 0)
|
||||||
}
|
}
|
||||||
@@ -87,7 +97,8 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||||||
selectedDual.alpha = 0.33f
|
selectedDual.alpha = 0.33f
|
||||||
selectedDual = imageButton
|
selectedDual = imageButton
|
||||||
selectedDual.alpha = 1f
|
selectedDual.alpha = 1f
|
||||||
settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic
|
settings.dualPageMode = CurrentReaderSettings.DualPageModes[index]
|
||||||
|
?: CurrentReaderSettings.DualPageModes.Automatic
|
||||||
binding.readerDualPageText.text = settings.dualPageMode.toString()
|
binding.readerDualPageText.text = settings.dualPageMode.toString()
|
||||||
activity.applySettings()
|
activity.applySettings()
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user