mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-21 22:13:57 +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"
|
||||||
@@ -170,15 +184,32 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="dantotsu"/>
|
<data android:scheme="dantotsu" />
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
<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,29 +245,30 @@
|
|||||||
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"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="Aani.dantotsu.ACTION_ALARM"/>
|
<action android:name="Aani.dantotsu.ACTION_ALARM" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
@@ -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"
|
||||||
<intent-filter>
|
android:permission="android.permission.BIND_REMOTEVIEWS"
|
||||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
|
android:exported="true" />
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<service
|
||||||
|
android:name=".download.video.ExoplayerDownloadService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</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)
|
||||||
@@ -36,24 +53,67 @@ class App : MultiDexApplication() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
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 {
|
||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
@@ -81,7 +141,7 @@ class App : MultiDexApplication() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: App? = null
|
private var instance: App? = null
|
||||||
var context : Context? = null
|
var context: Context? = null
|
||||||
fun currentContext(): Context? {
|
fun currentContext(): Context? {
|
||||||
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +148,10 @@ 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)
|
||||||
saveData("ui_settings", this)
|
?: UserInterfaceSettings().apply {
|
||||||
}
|
saveData("ui_settings", this)
|
||||||
|
}
|
||||||
uiSettings.darkMode.apply {
|
uiSettings.darkMode.apply {
|
||||||
AppCompatDelegate.setDefaultNightMode(
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
when (this) {
|
when (this) {
|
||||||
@@ -146,9 +163,10 @@ 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))
|
||||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
?.apply {
|
||||||
}
|
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||||
|
}
|
||||||
}
|
}
|
||||||
a.hideStatusBar()
|
a.hideStatusBar()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
@@ -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,25 +228,24 @@ 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 {
|
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
||||||
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
||||||
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
||||||
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
||||||
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
cap.hasTransport(TRANSPORT_USB) ||
|
||||||
cap.hasTransport(TRANSPORT_USB) ||
|
cap.hasTransport(TRANSPORT_VPN) ||
|
||||||
cap.hasTransport(TRANSPORT_VPN) ||
|
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
|
||||||
|
|
||||||
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
|
||||||
@@ -413,13 +463,18 @@ fun MutableList<ShowResponse>.sortByTitle(string: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun String.findBetween(a: String, b: String): String? {
|
fun String.findBetween(a: String, b: String): String? {
|
||||||
val string = substringAfter(a, "").substringBefore(b,"")
|
val string = substringAfter(a, "").substringBefore(b, "")
|
||||||
return string.ifEmpty { null }
|
return string.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ImageView.loadImage(url: String?, size: Int = 0) {
|
fun ImageView.loadImage(url: String?, size: Int = 0) {
|
||||||
if (!url.isNullOrEmpty()) {
|
if (!url.isNullOrEmpty()) {
|
||||||
loadImage(FileUrl(url), size)
|
val localFile = File(url)
|
||||||
|
if (localFile.exists()) {
|
||||||
|
loadLocalImage(localFile, size)
|
||||||
|
} else {
|
||||||
|
loadImage(FileUrl(url), size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +482,17 @@ 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,41 +799,52 @@ 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) {
|
||||||
if (s != null) {
|
try { //I have no idea why this sometimes crashes for some people...
|
||||||
(activity ?: currActivity())?.apply {
|
if (s != null) {
|
||||||
runOnUiThread {
|
(activity ?: currActivity())?.apply {
|
||||||
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG)
|
runOnUiThread {
|
||||||
snackBar.view.apply {
|
val snackBar = Snackbar.make(
|
||||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
window.decorView.findViewById(android.R.id.content),
|
||||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
s,
|
||||||
width = WRAP_CONTENT
|
Snackbar.LENGTH_SHORT
|
||||||
}
|
)
|
||||||
translationY = -(navBarHeight.dp + 32f)
|
snackBar.view.apply {
|
||||||
translationZ = 32f
|
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||||
updatePadding(16f.px, right = 16f.px)
|
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||||
setOnClickListener {
|
width = WRAP_CONTENT
|
||||||
snackBar.dismiss()
|
}
|
||||||
}
|
translationY = -(navBarHeight.dp + 32f)
|
||||||
setOnLongClickListener {
|
translationZ = 32f
|
||||||
copyToClipboard(clipboard ?: s, false)
|
updatePadding(16f.px, right = 16f.px)
|
||||||
toast(getString(R.string.copied_to_clipboard))
|
setOnClickListener {
|
||||||
true
|
snackBar.dismiss()
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
copyToClipboard(clipboard ?: s, false)
|
||||||
|
toast(getString(R.string.copied_to_clipboard))
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
snackBar.show()
|
||||||
}
|
}
|
||||||
snackBar.show()
|
|
||||||
}
|
}
|
||||||
|
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,16 +865,21 @@ 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 =
|
||||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
return performClick()
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
}
|
return performClick()
|
||||||
})
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
@@ -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)
|
||||||
@@ -850,12 +949,39 @@ fun checkCountry(context: Context): Boolean {
|
|||||||
tz.equals("Asia/Kolkata", ignoreCase = true)
|
tz.equals("Asia/Kolkata", ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
TelephonyManager.SIM_STATE_READY -> {
|
TelephonyManager.SIM_STATE_READY -> {
|
||||||
val countryCodeValue = telMgr.networkCountryIso
|
val countryCodeValue = telMgr.networkCountryIso
|
||||||
countryCodeValue.equals("in", ignoreCase = true)
|
countryCodeValue.equals("in", ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,27 +161,52 @@ 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 {
|
||||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
binding.root.addView(splash.root)
|
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||||
(splash.splashImage.drawable as Animatable).start()
|
binding.root.addView(splash.root)
|
||||||
|
(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,
|
0f,
|
||||||
0f,
|
-splash.root.height.toFloat()
|
||||||
-splash.root.height.toFloat()
|
).apply {
|
||||||
).apply {
|
interpolator = AnticipateInterpolator()
|
||||||
interpolator = AnticipateInterpolator()
|
duration = 200L
|
||||||
duration = 200L
|
doOnEnd { binding.root.removeView(splash.root) }
|
||||||
doOnEnd { binding.root.removeView(splash.root) }
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,108 +214,149 @@ class MainActivity : AppCompatActivity() {
|
|||||||
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 {
|
} else {
|
||||||
val model: AnilistHomeViewModel by viewModels()
|
if (offlineMode) {
|
||||||
model.genres.observe(this) {
|
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||||
if (it != null) {
|
startActivity(Intent(this, NoInternet::class.java))
|
||||||
if (it) {
|
} else {
|
||||||
val navbar = binding.navbar
|
val model: AnilistHomeViewModel by viewModels()
|
||||||
bottomBar = navbar
|
model.genres.observe(this) { it ->
|
||||||
navbar.visibility = View.VISIBLE
|
if (it != null) {
|
||||||
binding.mainProgressBar.visibility = View.GONE
|
if (it) {
|
||||||
val mainViewPager = binding.viewpager
|
val navbar = binding.includedNavbar.navbar
|
||||||
mainViewPager.isUserInputEnabled = false
|
bottomBar = navbar
|
||||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
navbar.visibility = View.VISIBLE
|
||||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
binding.mainProgressBar.visibility = View.GONE
|
||||||
navbar.setOnTabSelectListener(object :
|
val mainViewPager = binding.viewpager
|
||||||
AnimatedBottomBar.OnTabSelectListener {
|
mainViewPager.isUserInputEnabled = false
|
||||||
override fun onTabSelected(
|
mainViewPager.adapter =
|
||||||
lastIndex: Int,
|
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||||
lastTab: AnimatedBottomBar.Tab?,
|
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||||
newIndex: Int,
|
navbar.setOnTabSelectListener(object :
|
||||||
newTab: AnimatedBottomBar.Tab
|
AnimatedBottomBar.OnTabSelectListener {
|
||||||
) {
|
override fun onTabSelected(
|
||||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
lastIndex: Int,
|
||||||
selectedOption = newIndex
|
lastTab: AnimatedBottomBar.Tab?,
|
||||||
mainViewPager.setCurrentItem(newIndex, false)
|
newIndex: Int,
|
||||||
}
|
newTab: AnimatedBottomBar.Tab
|
||||||
})
|
) {
|
||||||
navbar.selectTabAt(selectedOption)
|
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||||
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
|
selectedOption = newIndex
|
||||||
} else {
|
mainViewPager.setCurrentItem(newIndex, false)
|
||||||
binding.mainProgressBar.visibility = View.GONE
|
}
|
||||||
}
|
})
|
||||||
}
|
navbar.selectTabAt(selectedOption)
|
||||||
}
|
mainViewPager.post {
|
||||||
//Load Data
|
mainViewPager.setCurrentItem(
|
||||||
if (!load) {
|
selectedOption,
|
||||||
scope.launch(Dispatchers.IO) {
|
false
|
||||||
model.loadMain(this@MainActivity)
|
|
||||||
val id = intent.extras?.getInt("mediaId", 0)
|
|
||||||
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
|
||||||
val cont = intent.extras?.getBoolean("continue") ?: false
|
|
||||||
if (id != null && id != 0) {
|
|
||||||
val media = withContext(Dispatchers.IO) {
|
|
||||||
Anilist.query.getMedia(id, isMAL)
|
|
||||||
}
|
|
||||||
if (media != null) {
|
|
||||||
media.cameFromContinue = cont
|
|
||||||
startActivity(
|
|
||||||
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
|
||||||
.putExtra("media", media as Serializable)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delay(500)
|
|
||||||
startSubscription()
|
|
||||||
}
|
|
||||||
load = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
if (loadData<Boolean>("allow_opening_links", this) != true) {
|
|
||||||
CustomBottomDialog.newInstance().apply {
|
|
||||||
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
|
||||||
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
val markWon =
|
|
||||||
Markwon.builder(this@MainActivity)
|
|
||||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
|
||||||
markWon.setMarkdown(this, md)
|
|
||||||
})
|
|
||||||
|
|
||||||
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
|
||||||
saveData("allow_opening_links", true, this@MainActivity)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
|
||||||
saveData("allow_opening_links", true, this@MainActivity)
|
|
||||||
tryWith(true) {
|
|
||||||
startActivity(
|
|
||||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
|
||||||
.setData(Uri.parse("package:$packageName"))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
binding.mainProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}.show(supportFragmentManager, "dialog")
|
}
|
||||||
|
}
|
||||||
|
//Load Data
|
||||||
|
if (!load) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
model.loadMain(this@MainActivity)
|
||||||
|
val id = intent.extras?.getInt("mediaId", 0)
|
||||||
|
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
||||||
|
val cont = intent.extras?.getBoolean("continue") ?: false
|
||||||
|
if (id != null && id != 0) {
|
||||||
|
val media = withContext(Dispatchers.IO) {
|
||||||
|
Anilist.query.getMedia(id, isMAL)
|
||||||
|
}
|
||||||
|
if (media != null) {
|
||||||
|
media.cameFromContinue = cont
|
||||||
|
startActivity(
|
||||||
|
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
||||||
|
.putExtra("media", media as Serializable)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(500)
|
||||||
|
startSubscription()
|
||||||
|
}
|
||||||
|
load = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if (loadData<Boolean>("allow_opening_links", this) != true) {
|
||||||
|
CustomBottomDialog.newInstance().apply {
|
||||||
|
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
||||||
|
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
||||||
|
addView(TextView(this@MainActivity).apply {
|
||||||
|
val markWon =
|
||||||
|
Markwon.builder(this@MainActivity)
|
||||||
|
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||||
|
markWon.setMarkdown(this, md)
|
||||||
|
})
|
||||||
|
|
||||||
|
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
||||||
|
saveData("allow_opening_links", true, this@MainActivity)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
||||||
|
saveData("allow_opening_links", true, this@MainActivity)
|
||||||
|
tryWith(true) {
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||||
|
.setData(Uri.parse("package:$packageName"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.show(supportFragmentManager, "dialog")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//TODO: Remove this
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||||
|
val downloadCursor = index.getDownloads()
|
||||||
|
while (downloadCursor.moveToNext()) {
|
||||||
|
val download = downloadCursor.download
|
||||||
|
Log.e("Downloader", download.request.uri.toString())
|
||||||
|
Log.e("Downloader", download.request.id.toString())
|
||||||
|
Log.e("Downloader", download.request.mimeType.toString())
|
||||||
|
Log.e("Downloader", download.request.data.size.toString())
|
||||||
|
Log.e("Downloader", download.bytesDownloaded.toString())
|
||||||
|
Log.e("Downloader", download.state.toString())
|
||||||
|
Log.e("Downloader", download.failureReason.toString())
|
||||||
|
|
||||||
|
if (download.state == Download.STATE_FAILED) { //simple cleanup
|
||||||
|
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//ViewPager
|
//ViewPager
|
||||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
FragmentStateAdapter(fragmentManager, 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) {
|
||||||
@@ -202,28 +199,29 @@ fun OkHttpClient.Builder.addAdGuardDns() = (
|
|||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, String>? {
|
suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, String>? {
|
||||||
var map : Map<String,String>? = null
|
var map: Map<String, String>? = null
|
||||||
|
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
webViewDialog.callback = {
|
webViewDialog.callback = {
|
||||||
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)
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun webViewInterface(type: String, url: FileUrl): Map<String, String>? {
|
suspend fun webViewInterface(type: String, url: FileUrl): Map<String, String>? {
|
||||||
val webViewDialog: WebViewBottomDialog = when (type) {
|
val webViewDialog: WebViewBottomDialog = when (type) {
|
||||||
"Cloudflare" -> CloudFlare.newInstance(url)
|
"Cloudflare" -> CloudFlare.newInstance(url)
|
||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
return webViewInterface(webViewDialog)
|
return webViewInterface(webViewDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun webViewInterface(type: String, url: String): Map<String, String>? {
|
suspend fun webViewInterface(type: String, url: String): Map<String, String>? {
|
||||||
return webViewInterface(type,FileUrl(url))
|
return webViewInterface(type, FileUrl(url))
|
||||||
}
|
}
|
||||||
@@ -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,30 +10,35 @@ 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) {
|
||||||
if (Anilist.userid != null) {
|
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
?.getBoolean("incognito", false) ?: false
|
||||||
val a = number.toFloatOrNull()?.roundToInt()
|
if (!incognito) {
|
||||||
if (a != media.userProgress) {
|
if (Anilist.userid != null) {
|
||||||
Anilist.mutation.editList(
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
media.id,
|
val a = number.toFloatOrNull()?.toInt()
|
||||||
a,
|
if ((a ?: 0) > (media.userProgress ?: 0)) {
|
||||||
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
Anilist.mutation.editList(
|
||||||
)
|
media.id,
|
||||||
MAL.query.editList(
|
a,
|
||||||
media.idMAL,
|
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
||||||
media.anime != null,
|
)
|
||||||
a, null,
|
MAL.query.editList(
|
||||||
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
media.idMAL,
|
||||||
)
|
media.anime != null,
|
||||||
toast(currContext()?.getString(R.string.setting_progress, a))
|
a, null,
|
||||||
|
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
||||||
|
)
|
||||||
|
toast(currContext()?.getString(R.string.setting_progress, a))
|
||||||
|
}
|
||||||
|
media.userProgress = a
|
||||||
|
Refresh.all()
|
||||||
}
|
}
|
||||||
media.userProgress = a
|
} else {
|
||||||
Refresh.all()
|
toast(currContext()?.getString(R.string.login_anilist_account))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast(currContext()?.getString(R.string.login_anilist_account))
|
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(
|
||||||
@@ -51,11 +56,11 @@ object Anilist {
|
|||||||
private val cal: Calendar = Calendar.getInstance()
|
private val cal: Calendar = Calendar.getInstance()
|
||||||
private val currentYear = cal.get(Calendar.YEAR)
|
private val currentYear = cal.get(Calendar.YEAR)
|
||||||
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
|
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
|
||||||
0, 1, 2 -> 0
|
0, 1, 2 -> 0
|
||||||
3, 4, 5 -> 1
|
3, 4, 5 -> 1
|
||||||
6, 7, 8 -> 2
|
6, 7, 8 -> 2
|
||||||
9, 10, 11 -> 3
|
9, 10, 11 -> 3
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSeason(next: Boolean): Pair<String, Int> {
|
private fun getSeason(next: Boolean): Pair<String, Int> {
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class AnilistMutations {
|
|||||||
repeat: Int? = null,
|
repeat: Int? = null,
|
||||||
notes: String? = null,
|
notes: String? = null,
|
||||||
status: String? = null,
|
status: String? = null,
|
||||||
private:Boolean? = null,
|
private: Boolean? = null,
|
||||||
startedAt: FuzzyDate? = null,
|
startedAt: FuzzyDate? = null,
|
||||||
completedAt: FuzzyDate? = null,
|
completedAt: FuzzyDate? = null,
|
||||||
customList: List<String>? = null
|
customList: List<String>? = null
|
||||||
@@ -41,7 +41,7 @@ class AnilistMutations {
|
|||||||
${if (repeat != null) ""","repeat":$repeat""" else ""}
|
${if (repeat != null) ""","repeat":$repeat""" else ""}
|
||||||
${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""}
|
${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""}
|
||||||
${if (status != null) ""","status":"$status"""" else ""}
|
${if (status != null) ""","status":"$status"""" else ""}
|
||||||
${if (customList !=null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""}
|
${if (customList != null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""}
|
||||||
}""".replace("\n", "").replace(""" """, "")
|
}""".replace("\n", "").replace(""" """, "")
|
||||||
println(variables)
|
println(variables)
|
||||||
executeQuery<JsonObject>(query, variables, show = true)
|
executeQuery<JsonObject>(query, variables, show = true)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -113,9 +122,13 @@ class AnilistQueries {
|
|||||||
name = i.node?.name?.userPreferred,
|
name = i.node?.name?.userPreferred,
|
||||||
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 =
|
||||||
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
i.url?.split("/")?.getOrNull(3)
|
||||||
|
|
||||||
|
"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,7 +248,9 @@ class AnilistQueries {
|
|||||||
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
if (currContext()?.let { isOnline(it) } == true) {
|
||||||
|
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val mal = async {
|
val mal = async {
|
||||||
@@ -241,10 +263,10 @@ class AnilistQueries {
|
|||||||
return media
|
return media
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun continueMedia(type: String,planned:Boolean=false): ArrayList<Media> {
|
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
|
||||||
val returnArray = arrayListOf<Media>()
|
val returnArray = arrayListOf<Media>()
|
||||||
val map = mutableMapOf<Int, Media>()
|
val map = mutableMapOf<Int, Media>()
|
||||||
val statuses = if(!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING")
|
val statuses = if (!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING")
|
||||||
suspend fun repeat(status: String) {
|
suspend fun repeat(status: String) {
|
||||||
val response =
|
val response =
|
||||||
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """)
|
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """)
|
||||||
@@ -275,21 +297,21 @@ class AnilistQueries {
|
|||||||
var hasNextPage = true
|
var hasNextPage = true
|
||||||
var page = 0
|
var page = 0
|
||||||
|
|
||||||
suspend fun getNextPage(page:Int): List<Media> {
|
suspend fun getNextPage(page: Int): List<Media> {
|
||||||
val response =
|
val response =
|
||||||
executeQuery<Query.User>("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""")
|
executeQuery<Query.User>("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""")
|
||||||
val favourites = response?.data?.user?.favourites
|
val favourites = response?.data?.user?.favourites
|
||||||
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
||||||
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
||||||
return apiMediaList?.edges?.mapNotNull {
|
return apiMediaList?.edges?.mapNotNull {
|
||||||
it.node?.let { i->
|
it.node?.let { i ->
|
||||||
Media(i).apply { isFav = true }
|
Media(i).apply { isFav = true }
|
||||||
}
|
}
|
||||||
} ?: return listOf()
|
} ?: return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
val responseArray = arrayListOf<Media>()
|
val responseArray = arrayListOf<Media>()
|
||||||
while(hasNextPage){
|
while (hasNextPage) {
|
||||||
page++
|
page++
|
||||||
responseArray.addAll(getNextPage(page))
|
responseArray.addAll(getNextPage(page))
|
||||||
}
|
}
|
||||||
@@ -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>>()
|
||||||
@@ -388,22 +414,30 @@ class AnilistQueries {
|
|||||||
if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!!
|
if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!!
|
||||||
}
|
}
|
||||||
unsorted.forEach {
|
unsorted.forEach {
|
||||||
if(!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
||||||
}
|
}
|
||||||
|
|
||||||
sorted["Favourites"] = favMedia(anime)
|
sorted["Favourites"] = favMedia(anime)
|
||||||
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 ->
|
||||||
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
compareValuesBy(
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
{ it.userScore },
|
||||||
|
{ it.meanScore })
|
||||||
|
}
|
||||||
|
|
||||||
|
"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 })
|
||||||
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sorted
|
return sorted
|
||||||
@@ -559,18 +593,36 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||||
${if (season != null) ""","season":"$season"""" else ""}
|
${if (season != null) ""","season":"$season"""" else ""}
|
||||||
${if (search != null) ""","search":"$search"""" else ""}
|
${if (search != null) ""","search":"$search"""" else ""}
|
||||||
${if (sort!=null) ""","sort":"$sort"""" else ""}
|
${if (sort != null) ""","sort":"$sort"""" else ""}
|
||||||
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
||||||
${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(""" """, "")
|
||||||
@@ -622,7 +674,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
greater: Long = 0,
|
greater: Long = 0,
|
||||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||||
): MutableList<Media>? {
|
): MutableList<Media>? {
|
||||||
suspend fun execute(page:Int = 1):Page?{
|
suspend fun execute(page: Int = 1): Page? {
|
||||||
val query = """{
|
val query = """{
|
||||||
Page(page:$page,perPage:50) {
|
Page(page:$page,perPage:50) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
@@ -668,7 +720,7 @@ Page(page:$page,perPage:50) {
|
|||||||
}""".replace("\n", " ").replace(""" """, "")
|
}""".replace("\n", " ").replace(""" """, "")
|
||||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||||
}
|
}
|
||||||
if(smaller) {
|
if (smaller) {
|
||||||
val response = execute()?.airingSchedules ?: return null
|
val response = execute()?.airingSchedules ?: return null
|
||||||
val idArr = mutableListOf<Int>()
|
val idArr = mutableListOf<Int>()
|
||||||
val listOnly = loadData("recently_list_only") ?: false
|
val listOnly = loadData("recently_list_only") ?: false
|
||||||
@@ -682,11 +734,11 @@ Page(page:$page,perPage:50) {
|
|||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
}else{
|
} else {
|
||||||
var i = 1
|
var i = 1
|
||||||
val list = mutableListOf<Media>()
|
val list = mutableListOf<Media>()
|
||||||
var res : Page? = null
|
var res: Page? = null
|
||||||
suspend fun next(){
|
suspend fun next() {
|
||||||
res = execute(i)
|
res = execute(i)
|
||||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
||||||
j.media?.let {
|
j.media?.let {
|
||||||
@@ -694,10 +746,10 @@ Page(page:$page,perPage:50) {
|
|||||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
}?: listOf())
|
} ?: listOf())
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
while (res?.pageInfo?.hasNextPage == true){
|
while (res?.pageInfo?.hasNextPage == true) {
|
||||||
next()
|
next()
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
@@ -822,19 +874,20 @@ 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 =
|
||||||
it.edges?.forEach { i ->
|
executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
||||||
i.node?.apply {
|
it.edges?.forEach { i ->
|
||||||
val status = status.toString()
|
i.node?.apply {
|
||||||
val year = startDate?.year?.toString() ?: "TBA"
|
val status = status.toString()
|
||||||
val title = if (status != "CANCELLED") year else status
|
val year = startDate?.year?.toString() ?: "TBA"
|
||||||
if (!yearMedia.containsKey(title))
|
val title = if (status != "CANCELLED") year else status
|
||||||
yearMedia[title] = arrayListOf()
|
if (!yearMedia.containsKey(title))
|
||||||
yearMedia[title]?.add(Media(this))
|
yearMedia[title] = arrayListOf()
|
||||||
|
yearMedia[title]?.add(Media(this))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
it.pageInfo?.hasNextPage == true
|
||||||
it.pageInfo?.hasNextPage == true
|
} ?: false
|
||||||
} ?: false
|
|
||||||
}
|
}
|
||||||
if (yearMedia.contains("CANCELLED")) {
|
if (yearMedia.contains("CANCELLED")) {
|
||||||
val a = yearMedia["CANCELLED"]!!
|
val a = yearMedia["CANCELLED"]!!
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,39 +45,57 @@ suspend fun getUserId(context: Context, block: () -> Unit) {
|
|||||||
}
|
}
|
||||||
} else true
|
} else true
|
||||||
|
|
||||||
if(anilist) block.invoke()
|
if (anilist) block.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
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,27 +50,37 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeChip(chip: SearchChip) {
|
fun removeChip(chip: SearchChip) {
|
||||||
when (chip.type) {
|
when (chip.type) {
|
||||||
"SORT" -> sort = null
|
"SORT" -> sort = null
|
||||||
"FORMAT" -> format = null
|
"FORMAT" -> format = null
|
||||||
"SEASON" -> season = null
|
"SEASON" -> season = null
|
||||||
"SEASON_YEAR" -> seasonYear = null
|
"SEASON_YEAR" -> seasonYear = null
|
||||||
"GENRE" -> genres?.remove(chip.text)
|
"GENRE" -> genres?.remove(chip.text)
|
||||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||||
"TAG" -> tags?.remove(chip.text)
|
"TAG" -> tags?.remove(chip.text)
|
||||||
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,23 +3,24 @@ package ani.dantotsu.connections.anilist.api
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
class Query{
|
class Query {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Viewer(
|
data class Viewer(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Viewer")
|
@SerialName("Viewer")
|
||||||
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")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Media")
|
@SerialName("Media")
|
||||||
@@ -30,12 +31,12 @@ class Query{
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Page(
|
data class Page(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Page")
|
@SerialName("Page")
|
||||||
val page : ani.dantotsu.connections.anilist.api.Page?
|
val page: ani.dantotsu.connections.anilist.api.Page?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// data class AiringSchedule(
|
// data class AiringSchedule(
|
||||||
@@ -49,8 +50,8 @@ class Query{
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Character(
|
data class Character(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@@ -63,7 +64,7 @@ class Query{
|
|||||||
data class Studio(
|
data class Studio(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Studio")
|
@SerialName("Studio")
|
||||||
@@ -76,7 +77,7 @@ class Query{
|
|||||||
data class Author(
|
data class Author(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Staff")
|
@SerialName("Staff")
|
||||||
@@ -95,8 +96,8 @@ class Query{
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MediaListCollection(
|
data class MediaListCollection(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("MediaListCollection")
|
@SerialName("MediaListCollection")
|
||||||
@@ -108,7 +109,7 @@ class Query{
|
|||||||
data class GenreCollection(
|
data class GenreCollection(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("GenreCollection")
|
@SerialName("GenreCollection")
|
||||||
@@ -120,7 +121,7 @@ class Query{
|
|||||||
data class MediaTagCollection(
|
data class MediaTagCollection(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("MediaTagCollection")
|
@SerialName("MediaTagCollection")
|
||||||
@@ -132,7 +133,7 @@ class Query{
|
|||||||
data class User(
|
data class User(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("User")
|
@SerialName("User")
|
||||||
|
|||||||
@@ -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,16 +31,21 @@ 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 {
|
||||||
return listOfNotNull(
|
return listOfNotNull(
|
||||||
year?.let {"year:$it"},
|
year?.let { "year:$it" },
|
||||||
month?.let {"month:$it"},
|
month?.let { "month:$it" },
|
||||||
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(
|
||||||
@@ -46,7 +53,7 @@ data class FuzzyDate(
|
|||||||
month?.toString()?.padStart(2, padding),
|
month?.toString()?.padStart(2, padding),
|
||||||
day?.toString()?.padStart(2, padding)
|
day?.toString()?.padStart(2, padding)
|
||||||
)
|
)
|
||||||
return values.takeWhile {it is String}.joinToString("-")
|
return values.takeWhile { it is String }.joinToString("-")
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun toInt(): Int {
|
// fun toInt(): Int {
|
||||||
@@ -54,8 +61,8 @@ data class FuzzyDate(
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
override fun compareTo(other: FuzzyDate): Int = when {
|
override fun compareTo(other: FuzzyDate): Int = when {
|
||||||
year != other.year -> (year ?: 0) - (other.year ?: 0)
|
year != other.year -> (year ?: 0) - (other.year ?: 0)
|
||||||
month != other.month -> (month ?: 0) - (other.month ?: 0)
|
month != other.month -> (month ?: 0) - (other.month ?: 0)
|
||||||
else -> (day ?: 0) - (other.day ?: 0)
|
else -> (day ?: 0) - (other.day ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ data class Media(
|
|||||||
@SerialName("characters") var characters: CharacterConnection?,
|
@SerialName("characters") var characters: CharacterConnection?,
|
||||||
|
|
||||||
// The staff who produced the media
|
// The staff who produced the media
|
||||||
@SerialName("staffPreview") var staff: StaffConnection?,
|
@SerialName("staffPreview") var staff: StaffConnection?,
|
||||||
|
|
||||||
// The companies who produced the media
|
// The companies who produced the media
|
||||||
@SerialName("studios") var studios: StudioConnection?,
|
@SerialName("studios") var studios: StudioConnection?,
|
||||||
@@ -292,7 +292,7 @@ data class MediaList(
|
|||||||
@SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?,
|
@SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?,
|
||||||
|
|
||||||
// Map of booleans for which custom lists the entry are in
|
// Map of booleans for which custom lists the entry are in
|
||||||
@SerialName("customLists") var customLists: Map<String,Boolean>?,
|
@SerialName("customLists") var customLists: Map<String, Boolean>?,
|
||||||
|
|
||||||
// Map of advanced scores with name keys
|
// Map of advanced scores with name keys
|
||||||
// @SerialName("advancedScores") var advancedScores: Json?,
|
// @SerialName("advancedScores") var advancedScores: Json?,
|
||||||
@@ -355,7 +355,7 @@ data class MediaTrailer(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MediaTagCollection(
|
data class MediaTagCollection(
|
||||||
@SerialName("tags") var tags : List<MediaTag>?
|
@SerialName("tags") var tags: List<MediaTag>?
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -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>?,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ data class Staff(
|
|||||||
@SerialName("id") var id: Int,
|
@SerialName("id") var id: Int,
|
||||||
|
|
||||||
// The names of the staff member
|
// The names of the staff member
|
||||||
@SerialName("name") var name: StaffName?,
|
@SerialName("name") var name: StaffName?,
|
||||||
|
|
||||||
// The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan, Finnish, Turkish, Dutch, Swedish, Thai, Tagalog, Malaysian, Indonesian, Vietnamese, Nepali, Hindi, Urdu
|
// The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan, Finnish, Turkish, Dutch, Swedish, Thai, Tagalog, Malaysian, Indonesian, Vietnamese, Nepali, Hindi, Urdu
|
||||||
@SerialName("languageV2") var languageV2: String?,
|
@SerialName("languageV2") var languageV2: String?,
|
||||||
@@ -80,8 +80,8 @@ data class Staff(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StaffName (
|
data class StaffName(
|
||||||
var userPreferred:String?
|
var userPreferred: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -96,6 +96,6 @@ data class StaffConnection(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StaffEdge(
|
data class StaffEdge(
|
||||||
var role:String?,
|
var role: String?,
|
||||||
var node: Staff?
|
var node: Staff?
|
||||||
)
|
)
|
||||||
@@ -80,10 +80,10 @@ data class UserOptions(
|
|||||||
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
|
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
|
||||||
|
|
||||||
// Whether the user receives notifications when a show they are watching aires
|
// Whether the user receives notifications when a show they are watching aires
|
||||||
@SerialName("airingNotifications") var airingNotifications: Boolean?,
|
@SerialName("airingNotifications") var airingNotifications: Boolean?,
|
||||||
//
|
//
|
||||||
// Profile highlight color (blue, purple, pink, orange, red, green, gray)
|
// Profile highlight color (blue, purple, pink, orange, red, green, gray)
|
||||||
@SerialName("profileColor") var profileColor: String?,
|
@SerialName("profileColor") var profileColor: String?,
|
||||||
//
|
//
|
||||||
// // Notification options
|
// // Notification options
|
||||||
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
|
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -60,17 +57,7 @@ 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 {
|
||||||
(function() {
|
// Check if the URL is the one expected after a successful login
|
||||||
const wreq = webpackChunkdiscord_app.push([[Symbol()], {}, w => w])
|
if (request?.url.toString() != "https://discord.com/login") {
|
||||||
webpackChunkdiscord_app.pop()
|
// Delay the script execution to ensure the page is fully loaded
|
||||||
const token = Object.values(wreq.c).find(m => m.exports?.Z?.getToken).exports.Z.getToken();
|
view?.postDelayed({
|
||||||
return token;
|
view.evaluateJavascript(
|
||||||
})()
|
"""
|
||||||
""".trimIndent()){
|
(function() {
|
||||||
login(it.trim('"'))
|
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();
|
||||||
}
|
return wreq;
|
||||||
|
})()
|
||||||
|
""".trimIndent()
|
||||||
|
) { 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,205 +17,73 @@ 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
|
|
||||||
data class KizzyApi(val id: String)
|
|
||||||
val api = "https://kizzy-api.vercel.app/image?url="
|
|
||||||
private suspend fun String.discordUrl(): String? {
|
|
||||||
if (startsWith("mp:")) return this
|
|
||||||
val json = app.get("$api$this").parsedSafe<KizzyApi>()
|
|
||||||
return json?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendIdentify() {
|
|
||||||
val response = Identity.Response(
|
|
||||||
op = 2,
|
|
||||||
d = Identity(
|
|
||||||
token = token,
|
|
||||||
properties = Identity.Properties(
|
|
||||||
os = "windows",
|
|
||||||
browser = "Chrome",
|
|
||||||
device = "disco"
|
|
||||||
),
|
|
||||||
compress = false,
|
|
||||||
intents = 0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
webSocket.send(json.encodeToString(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun send(block: RPC.() -> Unit) {
|
@Serializable
|
||||||
block.invoke(this)
|
data class KizzyApi(val id: String)
|
||||||
send()
|
|
||||||
}
|
|
||||||
|
|
||||||
var started = false
|
val api = "https://kizzy-api.vercel.app/image?url="
|
||||||
var whenStarted: ((User) -> Unit)? = null
|
private suspend fun String.discordUrl(): String? {
|
||||||
|
if (startsWith("mp:")) return this
|
||||||
|
val json = app.get("$api$this").parsedSafe<KizzyApi>()
|
||||||
|
return json?.id
|
||||||
|
}
|
||||||
|
|
||||||
fun send() {
|
suspend fun createPresence(data: RPCData): String {
|
||||||
val send = {
|
val json = Json {
|
||||||
CoroutineScope(coroutineContext).launch {
|
encodeDefaults = true
|
||||||
webSocket.send(createPresence())
|
allowStructuredMapKeys = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
}
|
return json.encodeToString(Presence.Response(
|
||||||
if (!started) whenStarted = {
|
3,
|
||||||
send.invoke()
|
Presence(
|
||||||
whenStarted = null
|
activities = listOf(
|
||||||
}
|
Activity(
|
||||||
else send.invoke()
|
name = data.activityName,
|
||||||
}
|
state = data.state,
|
||||||
|
details = data.details,
|
||||||
fun close() {
|
type = data.type?.ordinal,
|
||||||
webSocket.send(
|
timestamps = if (data.startTimestamp != null)
|
||||||
json.encodeToString(
|
Activity.Timestamps(data.startTimestamp, data.stopTimestamp)
|
||||||
Presence.Response(
|
else null,
|
||||||
3,
|
assets = Activity.Assets(
|
||||||
Presence(status = "offline")
|
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,8 +2,9 @@ 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")
|
||||||
val applicationId: String? = null,
|
val applicationId: String? = null,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ data class Identity(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Response (
|
data class Response(
|
||||||
val op: Long,
|
val op: Long,
|
||||||
val d: Identity
|
val d: Identity
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Properties (
|
data class Properties(
|
||||||
@SerialName("\$os")
|
@SerialName("\$os")
|
||||||
val os: String,
|
val os: String,
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package ani.dantotsu.connections.discord.serializers
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Presence (
|
data class Presence(
|
||||||
val activities: List<Activity> = listOf(),
|
val activities: List<Activity> = listOf(),
|
||||||
val afk: Boolean = true,
|
val afk: Boolean = true,
|
||||||
val since: Long? = null,
|
val since: Long? = null,
|
||||||
val status: String? = null
|
val status: String? = null
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Response (
|
data class Response(
|
||||||
val op: Long,
|
val op: Long,
|
||||||
val d: Presence
|
val d: Presence
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -70,7 +70,7 @@ data class User (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun userAvatar():String{
|
fun userAvatar(): String {
|
||||||
return "https://cdn.discordapp.com/avatars/$id/$avatar.png"
|
return "https://cdn.discordapp.com/avatars/$id/$avatar.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,9 +52,8 @@ 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
|
||||||
@@ -94,6 +100,6 @@ object MAL {
|
|||||||
@SerialName("expires_in") var expiresIn: Long,
|
@SerialName("expires_in") var expiresIn: Long,
|
||||||
@SerialName("access_token") val accessToken: String,
|
@SerialName("access_token") val accessToken: String,
|
||||||
@SerialName("refresh_token") val refreshToken: String,
|
@SerialName("refresh_token") val refreshToken: String,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -43,18 +43,18 @@ class MALQueries {
|
|||||||
start: FuzzyDate? = null,
|
start: FuzzyDate? = null,
|
||||||
end: FuzzyDate? = null
|
end: FuzzyDate? = null
|
||||||
) {
|
) {
|
||||||
if(idMAL==null) return
|
if (idMAL == null) return
|
||||||
val data = mutableMapOf("status" to convertStatus(isAnime, status))
|
val data = mutableMapOf("status" to convertStatus(isAnime, status))
|
||||||
if (progress != null)
|
if (progress != null)
|
||||||
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
|
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
|
||||||
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
|
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
|
||||||
if (score != null)
|
if (score != null)
|
||||||
data["score"] = score.div(10).toString()
|
data["score"] = score.div(10).toString()
|
||||||
if(rewatch!=null)
|
if (rewatch != null)
|
||||||
data[if(isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
data[if (isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
||||||
if(start!=null)
|
if (start != null)
|
||||||
data["start_date"] = start.toMALString()
|
data["start_date"] = start.toMALString()
|
||||||
if(end!=null)
|
if (end != null)
|
||||||
data["finish_date"] = end.toMALString()
|
data["finish_date"] = end.toMALString()
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
client.put(
|
client.put(
|
||||||
@@ -65,8 +65,8 @@ class MALQueries {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteList(isAnime: Boolean, idMAL: Int?){
|
suspend fun deleteList(isAnime: Boolean, idMAL: Int?) {
|
||||||
if(idMAL==null) return
|
if (idMAL == null) return
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
client.delete(
|
client.delete(
|
||||||
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
|
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
|
||||||
|
|||||||
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)
|
||||||
}
|
}
|
||||||
@@ -49,7 +72,7 @@ object Helper {
|
|||||||
val mimeType = when (video.format) {
|
val mimeType = when (video.format) {
|
||||||
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||||
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
||||||
else -> MimeTypes.APPLICATION_MP4
|
else -> MimeTypes.APPLICATION_MP4
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
||||||
@@ -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()
|
||||||
@@ -75,28 +99,15 @@ object Helper {
|
|||||||
DefaultRenderersFactory(context),
|
DefaultRenderersFactory(context),
|
||||||
dataSourceFactory
|
dataSourceFactory
|
||||||
)
|
)
|
||||||
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,17 +130,25 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var onSeasonClick : ((Int)->Unit)
|
lateinit var onSeasonClick: ((Int) -> Unit)
|
||||||
lateinit var onSeasonLongClick : ((Int)->Boolean)
|
lateinit var onSeasonLongClick: ((Int) -> Boolean)
|
||||||
lateinit var onIncludeListClick : ((Boolean)->Unit)
|
lateinit var onIncludeListClick: ((Boolean) -> Unit)
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
@@ -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)
|
||||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration).start()
|
.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,16 +123,23 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var onIncludeListClick : ((Boolean)->Unit)
|
lateinit var onIncludeListClick: ((Boolean) -> Unit)
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
@@ -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) {
|
||||||
bottomMargin = navBarHeight
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
binding.refreshButton.setOnClickListener {
|
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
if (isOnline(this)) {
|
.getBoolean("colorOverflow", false)
|
||||||
startMainActivity(this)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val navbar = binding.includedNavbar.navbar
|
||||||
|
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,14 +94,15 @@ 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 onTabReselected(tab: TabLayout.Tab?) { }
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
model.getCalendar().observe(this) {
|
model.getCalendar().observe(this) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
binding.listProgressBar.visibility = View.GONE
|
binding.listProgressBar.visibility = View.GONE
|
||||||
binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true,this)
|
binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true, this)
|
||||||
val keys = it.keys.toList()
|
val keys = it.keys.toList()
|
||||||
val values = it.values.toList()
|
val values = it.values.toList()
|
||||||
val savedTab = this.selectedTabIdx
|
val savedTab = this.selectedTabIdx
|
||||||
@@ -67,4 +124,4 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,14 +88,15 @@ 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)
|
||||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
override fun getSpanSize(position: Int): Int {
|
override fun getSpanSize(position: Int): Int {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> gridSize
|
0 -> gridSize
|
||||||
else -> 1
|
else -> 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,20 +24,22 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
|||||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val desc =
|
val desc =
|
||||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
||||||
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
||||||
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when(character.gender){
|
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||||
"Male" -> currActivity()!!.getString(R.string.male)
|
"Male" -> currActivity()!!.getString(R.string.male)
|
||||||
"Female" -> currActivity()!!.getString(R.string.female)
|
"Female" -> currActivity()!!.getString(R.string.female)
|
||||||
else -> character.gender
|
else -> character.gender
|
||||||
} 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(
|
||||||
@@ -48,15 +49,15 @@ class GenreAdapter(
|
|||||||
.putExtra("sortBy", Anilist.sortBy[2])
|
.putExtra("sortBy", Anilist.sortBy[2])
|
||||||
.putExtra("search", true)
|
.putExtra("search", true)
|
||||||
.also {
|
.also {
|
||||||
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
|
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
|
||||||
if (!Anilist.adult) Toast.makeText(
|
if (!Anilist.adult) Toast.makeText(
|
||||||
itemView.context,
|
itemView.context,
|
||||||
currActivity()?.getString(R.string.content_18),
|
currActivity()?.getString(R.string.content_18),
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
it.putExtra("hentai", true)
|
it.putExtra("hentai", true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ data class Media(
|
|||||||
var userUpdatedAt: Long? = null,
|
var userUpdatedAt: Long? = null,
|
||||||
var userStartedAt: FuzzyDate = FuzzyDate(),
|
var userStartedAt: FuzzyDate = FuzzyDate(),
|
||||||
var userCompletedAt: FuzzyDate = FuzzyDate(),
|
var userCompletedAt: FuzzyDate = FuzzyDate(),
|
||||||
var inCustomListsOf: MutableMap<String, Boolean>?= null,
|
var inCustomListsOf: MutableMap<String, Boolean>? = null,
|
||||||
var userFavOrder: Int? = null,
|
var userFavOrder: Int? = null,
|
||||||
|
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
@@ -69,7 +70,7 @@ data class Media(
|
|||||||
var shareLink: String? = null,
|
var shareLink: String? = null,
|
||||||
var selected: Selected? = null,
|
var selected: Selected? = null,
|
||||||
|
|
||||||
var idKitsu: String?=null,
|
var idKitsu: String? = null,
|
||||||
|
|
||||||
var cameFromContinue: Boolean = false
|
var cameFromContinue: Boolean = false
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
@@ -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,20 +44,43 @@ 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),
|
||||||
3 -> MediaPageSmallViewHolder(
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
1 -> MediaLargeViewHolder(
|
||||||
|
ItemMediaLargeBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
2 -> MediaPageViewHolder(
|
||||||
|
ItemMediaPageBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
3 -> MediaPageSmallViewHolder(
|
||||||
ItemMediaPageSmallBinding.inflate(
|
ItemMediaPageSmallBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
parent,
|
parent,
|
||||||
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
|
||||||
else currActivity()!!.getString(R.string.episode_singular)
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.episode_plural)
|
||||||
|
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 }
|
||||||
binding.mediaTitle.text = media.userPreferredName
|
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.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
|
||||||
@@ -234,7 +295,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
if (it != null) {
|
if (it != null) {
|
||||||
media = it
|
media = it
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if(media.isFav!=favButton?.clicked) favButton?.clicked()
|
if (media.isFav != favButton?.clicked) favButton?.clicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mediaNotify.setOnClickListener {
|
binding.mediaNotify.setOnClickListener {
|
||||||
@@ -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(
|
||||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
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)
|
||||||
|
}
|
||||||
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
|
||||||
@@ -303,9 +372,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
private fun selectFromID(id: Int) {
|
private fun selectFromID(id: Int) {
|
||||||
when (id) {
|
when (id) {
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -329,9 +399,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -342,13 +413,14 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
override fun getItemCount(): Int = 2
|
override fun getItemCount(): Int = 2
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = when (position){
|
override fun createFragment(position: Int): Fragment = when (position) {
|
||||||
0 -> MediaInfoFragment()
|
0 -> MediaInfoFragment()
|
||||||
1 -> when(media){
|
1 -> when (media) {
|
||||||
SupportedMedia.ANIME -> AnimeWatchFragment()
|
SupportedMedia.ANIME -> AnimeWatchFragment()
|
||||||
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) {
|
||||||
image,
|
ObjectAnimator.ofArgb(
|
||||||
"ColorFilter",
|
image,
|
||||||
ContextCompat.getColor(context, c2),
|
"ColorFilter",
|
||||||
ContextCompat.getColor(context, c1)
|
ContextCompat.getColor(context, c2),
|
||||||
).setDuration(200).start()
|
ContextCompat.getColor(context, c1)
|
||||||
|
).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))
|
||||||
@@ -284,7 +329,7 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun autoSearchNovels(media: Media) {
|
suspend fun autoSearchNovels(media: Media) {
|
||||||
val source = novelSources[media.selected?.sourceIndex?:0]
|
val source = novelSources[media.selected?.sourceIndex ?: 0]
|
||||||
tryWithSuspend(post = true) {
|
tryWithSuspend(post = true) {
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
novelResponses.postValue(source.sortedSearch(media))
|
novelResponses.postValue(source.sortedSearch(media))
|
||||||
@@ -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,12 +60,13 @@ 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 }
|
||||||
|
|
||||||
model.scrolledToTop.observe(viewLifecycleOwner){
|
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||||
if(it) binding.mediaInfoScroll.scrollTo(0,0)
|
if (it) binding.mediaInfoScroll.scrollTo(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
@@ -68,56 +74,62 @@ class MediaInfoFragment : Fragment() {
|
|||||||
loaded = true
|
loaded = true
|
||||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||||
binding.mediaInfoContainer.visibility = View.VISIBLE
|
binding.mediaInfoContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoName.text = "\t\t\t" + (media.name?:media.nameRomaji)
|
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
|
||||||
binding.mediaInfoName.setOnLongClickListener {
|
binding.mediaInfoName.setOnLongClickListener {
|
||||||
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
|
||||||
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
||||||
binding.mediaInfoEnd.text =media.endDate?.toString() ?: "??"
|
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
binding.mediaInfoDuration.text =
|
binding.mediaInfoDuration.text =
|
||||||
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
|
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
|
||||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoSeason.text =
|
binding.mediaInfoSeason.text =
|
||||||
(media.anime.season ?: "??")+ " " + (media.anime.seasonYear ?: "??")
|
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
|
||||||
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
|
||||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
if (!offline) {
|
||||||
ContextCompat.startActivity(
|
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||||
requireActivity(),
|
ContextCompat.startActivity(
|
||||||
Intent(activity, StudioActivity::class.java).putExtra(
|
requireActivity(),
|
||||||
"studio",
|
Intent(activity, StudioActivity::class.java).putExtra(
|
||||||
media.anime.mainStudio!! as Serializable
|
"studio",
|
||||||
),
|
media.anime.mainStudio!! as Serializable
|
||||||
null
|
),
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
if (!offline) {
|
||||||
ContextCompat.startActivity(
|
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||||
requireActivity(),
|
ContextCompat.startActivity(
|
||||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
requireActivity(),
|
||||||
"author",
|
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||||
media.anime.author!! as Serializable
|
"author",
|
||||||
),
|
media.anime.author!! as Serializable
|
||||||
null
|
),
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||||
@@ -131,15 +143,17 @@ 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
|
||||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
if (!offline) {
|
||||||
ContextCompat.startActivity(
|
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||||
requireActivity(),
|
ContextCompat.startActivity(
|
||||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
requireActivity(),
|
||||||
"author",
|
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||||
media.manga.author!! as Serializable
|
"author",
|
||||||
),
|
media.manga.author!! as Serializable
|
||||||
null
|
),
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,9 +50,13 @@ 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(
|
||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
@@ -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
|
||||||
@@ -245,7 +259,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Anilist.mutation.deleteList(id)
|
Anilist.mutation.deleteList(id)
|
||||||
MAL.query.deleteList(media?.anime!=null,media?.idMAL)
|
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||||
}
|
}
|
||||||
Refresh.all()
|
Refresh.all()
|
||||||
snackString(getString(R.string.deleted_from_list))
|
snackString(getString(R.string.deleted_from_list))
|
||||||
|
|||||||
@@ -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,26 +19,28 @@ 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)
|
|
||||||
fun getCalendar(): LiveData<Map<String,MutableList<Media>>> = calendar
|
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
|
||||||
|
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
||||||
suspend fun loadCalendar() {
|
suspend fun loadCalendar() {
|
||||||
val curr = System.currentTimeMillis()/1000
|
val curr = System.currentTimeMillis() / 1000
|
||||||
val res = Anilist.query.recentlyUpdated(false,curr-86400,curr+(86400*6))
|
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
|
||||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||||
val map = mutableMapOf<String,MutableList<Media>>()
|
val map = mutableMapOf<String, MutableList<Media>>()
|
||||||
val idMap = mutableMapOf<String,MutableList<Int>>()
|
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||||
res?.forEach {
|
res?.forEach {
|
||||||
val v = it.relation?.split(",")?.map { i-> i.toLong() }!!
|
val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
|
||||||
val dateInfo = df.format(Date(v[1]*1000))
|
val dateInfo = df.format(Date(v[1] * 1000))
|
||||||
val list = map.getOrPut(dateInfo) { mutableListOf() }
|
val list = map.getOrPut(dateInfo) { mutableListOf() }
|
||||||
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
|
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
|
||||||
it.relation = "Episode ${v[0]}"
|
it.relation = "Episode ${v[0]}"
|
||||||
if(!idList.contains(it.id)) {
|
if (!idList.contains(it.id)) {
|
||||||
idList.add(it.id)
|
idList.add(it.id)
|
||||||
list.add(it)
|
list.add(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.*
|
||||||
@@ -33,10 +35,12 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
private lateinit var concatAdapter: ConcatAdapter
|
private lateinit var concatAdapter: ConcatAdapter
|
||||||
|
|
||||||
lateinit var result: SearchResults
|
lateinit var result: SearchResults
|
||||||
lateinit var updateChips: (()->Unit)
|
lateinit var updateChips: (() -> Unit)
|
||||||
|
|
||||||
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,15 +78,15 @@ 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 {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> gridSize
|
0 -> gridSize
|
||||||
concatAdapter.itemCount - 1 -> gridSize
|
concatAdapter.itemCount - 1 -> gridSize
|
||||||
else -> when (style) {
|
else -> when (style) {
|
||||||
0 -> 1
|
0 -> 1
|
||||||
else -> gridSize
|
else -> gridSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +149,7 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
} else
|
} else
|
||||||
headerAdaptor.requestFocus?.run()
|
headerAdaptor.requestFocus?.run()
|
||||||
|
|
||||||
if(intent.getBooleanExtra("search",false)) search()
|
if (intent.getBooleanExtra("search", false)) search()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,7 +116,8 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.searchBar.setEndIconOnClickListener { searchTitle() }
|
binding.searchBar.setEndIconOnClickListener { searchTitle() }
|
||||||
@@ -127,7 +148,7 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
binding.searchList.apply {
|
binding.searchList.apply {
|
||||||
if (Anilist.userid != null) {
|
if (Anilist.userid != null) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
checkedState = when(listOnly){
|
checkedState = when (listOnly) {
|
||||||
null -> STATE_UNCHECKED
|
null -> STATE_UNCHECKED
|
||||||
true -> STATE_CHECKED
|
true -> STATE_CHECKED
|
||||||
false -> STATE_INDETERMINATE
|
false -> STATE_INDETERMINATE
|
||||||
@@ -135,10 +156,10 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
|
|
||||||
addOnCheckedStateChangedListener { _, state ->
|
addOnCheckedStateChangedListener { _, state ->
|
||||||
listOnly = when (state) {
|
listOnly = when (state) {
|
||||||
STATE_CHECKED -> true
|
STATE_CHECKED -> true
|
||||||
STATE_INDETERMINATE -> false
|
STATE_INDETERMINATE -> false
|
||||||
STATE_UNCHECKED -> null
|
STATE_UNCHECKED -> null
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,24 +133,25 @@ 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 =
|
||||||
val tag = chip.text.toString()
|
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
||||||
chip.isChecked = selectedTags.contains(tag)
|
val tag = chip.text.toString()
|
||||||
chip.isCloseIconVisible = exTags.contains(tag)
|
chip.isChecked = selectedTags.contains(tag)
|
||||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
chip.isCloseIconVisible = exTags.contains(tag)
|
||||||
if (isChecked) {
|
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||||
chip.isCloseIconVisible = false
|
if (isChecked) {
|
||||||
exTags.remove(tag)
|
chip.isCloseIconVisible = false
|
||||||
selectedTags.add(tag)
|
exTags.remove(tag)
|
||||||
} else
|
selectedTags.add(tag)
|
||||||
selectedTags.remove(tag)
|
} else
|
||||||
|
selectedTags.remove(tag)
|
||||||
|
}
|
||||||
|
chip.setOnLongClickListener {
|
||||||
|
chip.isChecked = false
|
||||||
|
chip.isCloseIconVisible = true
|
||||||
|
exTags.add(tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chip.setOnLongClickListener {
|
|
||||||
chip.isChecked = false
|
|
||||||
chip.isCloseIconVisible = true
|
|
||||||
exTags.add(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked ->
|
binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked ->
|
||||||
binding.searchFilterTags.layoutManager =
|
binding.searchFilterTags.layoutManager =
|
||||||
if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||||
@@ -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,7 +92,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
search()
|
search()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.searchBar.setEndIconOnClickListener { search() }
|
binding.searchBar.setEndIconOnClickListener { search() }
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ data class Anime(
|
|||||||
var ed: ArrayList<String> = arrayListOf(),
|
var ed: ArrayList<String> = arrayListOf(),
|
||||||
|
|
||||||
var mainStudio: Studio? = null,
|
var mainStudio: Studio? = null,
|
||||||
var author: Author?=null,
|
var author: Author? = null,
|
||||||
|
|
||||||
var youtube: String? = null,
|
var youtube: String? = null,
|
||||||
var nextAiringEpisode: Int? = null,
|
var nextAiringEpisode: Int? = null,
|
||||||
|
|||||||
@@ -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,14 +132,48 @@ 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, _ ->
|
||||||
subscribe = MediaDetailsActivity.PopImageButton(
|
// 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(
|
||||||
fragment.lifecycleScope,
|
fragment.lifecycleScope,
|
||||||
binding.animeSourceSubscribe,
|
binding.animeSourceSubscribe,
|
||||||
R.drawable.ic_round_notifications_active_24,
|
R.drawable.ic_round_notifications_active_24,
|
||||||
@@ -111,51 +188,107 @@ class AnimeWatchAdapter(
|
|||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
|
|
||||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||||
openSettings(fragment.requireContext(),getChannelId(true,media.id))
|
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
//Icons
|
//Nested Button
|
||||||
var reversed = media.selected!!.recyclerReversed
|
binding.animeNestedButton.setOnClickListener {
|
||||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
val dialogView =
|
||||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||||
binding.animeSourceTop.setOnClickListener {
|
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||||
reversed = !reversed
|
var refresh = false
|
||||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
var run = false
|
||||||
fragment.onIconPressed(style, reversed)
|
var reversed = media.selected!!.recyclerReversed
|
||||||
}
|
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
||||||
var selected = when (style) {
|
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||||
0 -> binding.animeSourceList
|
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||||
1 -> binding.animeSourceGrid
|
dialogBinding.animeSourceTop.setOnClickListener {
|
||||||
2 -> binding.animeSourceCompact
|
reversed = !reversed
|
||||||
else -> binding.animeSourceList
|
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||||
}
|
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||||
selected.alpha = 1f
|
run = true
|
||||||
fun selected(it: ImageView) {
|
}
|
||||||
selected.alpha = 0.33f
|
//Grids
|
||||||
selected = it
|
var selected = when (style) {
|
||||||
|
0 -> dialogBinding.animeSourceList
|
||||||
|
1 -> dialogBinding.animeSourceGrid
|
||||||
|
2 -> dialogBinding.animeSourceCompact
|
||||||
|
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: ImageButton) {
|
||||||
binding.animeSourceList.setOnClickListener {
|
selected.alpha = 0.33f
|
||||||
selected(it as ImageView)
|
selected = it
|
||||||
style = 0
|
selected.alpha = 1f
|
||||||
fragment.onIconPressed(style, reversed)
|
}
|
||||||
}
|
dialogBinding.animeSourceList.setOnClickListener {
|
||||||
binding.animeSourceGrid.setOnClickListener {
|
selected(it as ImageButton)
|
||||||
selected(it as ImageView)
|
style = 0
|
||||||
style = 1
|
dialogBinding.layoutText.text = "List"
|
||||||
fragment.onIconPressed(style, reversed)
|
run = true
|
||||||
}
|
}
|
||||||
binding.animeSourceCompact.setOnClickListener {
|
dialogBinding.animeSourceGrid.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageButton)
|
||||||
style = 2
|
style = 1
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.layoutText.text = "Grid"
|
||||||
}
|
run = true
|
||||||
|
}
|
||||||
|
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||||
|
selected(it as ImageButton)
|
||||||
|
style = 2
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribeButton(enabled : Boolean) {
|
fun subscribeButton(enabled: Boolean) {
|
||||||
subscribe?.enabled(enabled)
|
subscribe?.enabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,12 +419,43 @@ 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)
|
||||||
|
|
||||||
@@ -86,11 +131,11 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
val style = episodeAdapter.getItemViewType(position)
|
val style = episodeAdapter.getItemViewType(position)
|
||||||
|
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> maxGridSize
|
0 -> maxGridSize
|
||||||
else -> when (style) {
|
else -> when (style) {
|
||||||
0 -> maxGridSize
|
0 -> maxGridSize
|
||||||
1 -> 2
|
1 -> 2
|
||||||
2 -> 1
|
2 -> 1
|
||||||
else -> maxGridSize
|
else -> 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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +231,7 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
val limit = when {
|
val limit = when {
|
||||||
(divisions < 25) -> 25
|
(divisions < 25) -> 25
|
||||||
(divisions < 50) -> 50
|
(divisions < 50) -> 50
|
||||||
else -> 100
|
else -> 100
|
||||||
}
|
}
|
||||||
headerAdapter.clearChips()
|
headerAdapter.clearChips()
|
||||||
if (total > limit) {
|
if (total > limit) {
|
||||||
@@ -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,12 +14,13 @@ 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 extractorCallback: ((VideoExtractor) -> Unit)?=null,
|
@Transient var extractors: MutableList<VideoExtractor>? = null,
|
||||||
|
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
|
||||||
var allStreams: Boolean = false,
|
var allStreams: Boolean = false,
|
||||||
var watched: Long? = null,
|
var watched: Long? = null,
|
||||||
var maxLength: Long? = null,
|
var maxLength: Long? = null,
|
||||||
val extra: Map<String,String>?=null,
|
val extra: Map<String, String>? = null,
|
||||||
val sEpisode: eu.kanade.tachiyomi.animesource.model.SEpisode? = null
|
val sEpisode: eu.kanade.tachiyomi.animesource.model.SEpisode? = null
|
||||||
) : Serializable
|
) : Serializable
|
||||||
|
|
||||||
|
|||||||
@@ -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,24 +46,50 @@ 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(
|
||||||
2 -> EpisodeCompactViewHolder(
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
1 -> EpisodeGridViewHolder(
|
||||||
|
ItemEpisodeGridBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
2 -> EpisodeCompactViewHolder(
|
||||||
ItemEpisodeCompactBinding.inflate(
|
ItemEpisodeCompactBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
parent,
|
parent,
|
||||||
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()) {
|
||||||
@@ -110,12 +157,14 @@ class EpisodeAdapter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is EpisodeGridViewHolder -> {
|
is EpisodeGridViewHolder -> {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,11 +80,14 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
media = m
|
media = m
|
||||||
if (media != null && !loaded) {
|
if (media != null && !loaded) {
|
||||||
loaded = true
|
loaded = true
|
||||||
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,14 +104,22 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
val size = ep.extractors?.find { it.server.name == selected }?.videos?.size
|
val size =
|
||||||
if (size!=null && size >= media!!.selected!!.video) {
|
if (model.watchSources!!.isDownloadedSource(media!!.selected!!.sourceIndex)) {
|
||||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = selected
|
ep.extractors?.firstOrNull()?.videos?.size
|
||||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo = media!!.selected!!.video
|
} else {
|
||||||
|
ep.extractors?.find { it.server.name == selected }?.videos?.size
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size != null && size >= media!!.selected!!.video) {
|
||||||
|
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
|
||||||
|
selected
|
||||||
|
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo =
|
||||||
|
media!!.selected!!.video
|
||||||
startExoplayer(media!!)
|
startExoplayer(media!!)
|
||||||
} else fail()
|
} else fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ep.extractors.isNullOrEmpty()) {
|
if (ep.extractors.isNullOrEmpty()) {
|
||||||
model.getEpisode().observe(this) {
|
model.getEpisode().observe(this) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
@@ -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,29 +149,42 @@ 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) {
|
||||||
model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
|
model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
|
||||||
withContext(Dispatchers.Main){
|
withContext(Dispatchers.Main) {
|
||||||
binding.selectorProgressBar.visibility = View.GONE
|
binding.selectorProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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,83 +221,204 @@ 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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = links.size
|
override fun getItemCount(): Int = links.size
|
||||||
|
|
||||||
fun add(videoExtractor: VideoExtractor){
|
fun add(videoExtractor: VideoExtractor) {
|
||||||
if(videoExtractor.videos.isNotEmpty()) {
|
if (videoExtractor.videos.isNotEmpty()) {
|
||||||
links.add(videoExtractor)
|
links.add(videoExtractor)
|
||||||
notifyItemInserted(links.size - 1)
|
notifyItemInserted(links.size - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAll(extractors: List<VideoExtractor>?) {
|
fun addAll(extractors: List<VideoExtractor>?) {
|
||||||
links.addAll(extractors?:return)
|
links.addAll(extractors ?: return)
|
||||||
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 StreamViewHolder(val binding: ItemStreamBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class VideoAdapter(private val extractor : VideoExtractor) : RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() {
|
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.urlDownload.visibility = View.VISIBLE
|
||||||
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
|
} else {
|
||||||
binding.urlDownload.visibility = View.VISIBLE
|
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)
|
||||||
download(
|
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
||||||
requireActivity(),
|
val selectedVideo =
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||||
media!!.userPreferredName
|
|
||||||
)
|
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()
|
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(
|
||||||
|
requireActivity(),
|
||||||
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
||||||
|
media!!.userPreferredName
|
||||||
|
)
|
||||||
|
} 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
|
||||||
@@ -263,12 +429,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
itemView.setOnLongClickListener {
|
itemView.setOnLongClickListener {
|
||||||
val video = extractor.videos[bindingAdapterPosition]
|
val video = extractor.videos[bindingAdapterPosition]
|
||||||
val intent= Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
setDataAndType(Uri.parse(video.file.url),"video/*")
|
setDataAndType(Uri.parse(video.file.url), "video/*")
|
||||||
}
|
}
|
||||||
copyToClipboard(video.file.url,true)
|
copyToClipboard(video.file.url, true)
|
||||||
dismiss()
|
dismiss()
|
||||||
startActivity(Intent.createChooser(intent,"Open Video in :"))
|
startActivity(Intent.createChooser(intent, "Open Video in :"))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
@@ -60,7 +77,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
episode.selectedSubtitle = null
|
episode.selectedSubtitle = null
|
||||||
model.setEpisode(episode, "Subtitle")
|
model.setEpisode(episode, "Subtitle")
|
||||||
model.getMedia().observe(viewLifecycleOwner){media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
val mediaID: Int = media.id
|
val mediaID: Int = media.id
|
||||||
saveData("subLang_${mediaID}", "None", activity)
|
saveData("subLang_${mediaID}", "None", activity)
|
||||||
}
|
}
|
||||||
@@ -87,7 +104,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||||||
"pl-PL" -> "[pl-PL] Polish"
|
"pl-PL" -> "[pl-PL] Polish"
|
||||||
"ro-RO" -> "[ro-RO] Romanian"
|
"ro-RO" -> "[ro-RO] Romanian"
|
||||||
"sv-SE" -> "[sv-SE] Swedish"
|
"sv-SE" -> "[sv-SE] Swedish"
|
||||||
else -> if(subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language
|
else -> if (subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language
|
||||||
}
|
}
|
||||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
val mediaID: Int = media.id
|
val mediaID: Int = media.id
|
||||||
@@ -100,7 +117,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
episode.selectedSubtitle = position - 1
|
episode.selectedSubtitle = position - 1
|
||||||
model.setEpisode(episode, "Subtitle")
|
model.setEpisode(episode, "Subtitle")
|
||||||
model.getMedia().observe(viewLifecycleOwner){media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
val mediaID: Int = media.id
|
val mediaID: Int = media.id
|
||||||
saveData("subLang_${mediaID}", subtitles[position - 1].language, activity)
|
saveData("subLang_${mediaID}", subtitles[position - 1].language, activity)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ data class Manga(
|
|||||||
var selectedChapter: String? = null,
|
var selectedChapter: String? = null,
|
||||||
var chapters: MutableMap<String, MangaChapter>? = null,
|
var chapters: MutableMap<String, MangaChapter>? = null,
|
||||||
var slug: String? = null,
|
var slug: String? = null,
|
||||||
var author: Author?=null,
|
var author: Author? = null,
|
||||||
) : Serializable
|
) : Serializable
|
||||||
@@ -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
|
||||||
@@ -20,44 +22,59 @@ import java.io.FileOutputStream
|
|||||||
data class ImageData(
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,4 +323,4 @@ class MangaChapterAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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 {
|
} else {
|
||||||
null
|
val failedChapterNumberPattern: Pattern =
|
||||||
|
Pattern.compile(filedChapterNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val failedChapterNumberMatcher: Matcher =
|
||||||
|
failedChapterNumberPattern.matcher(text)
|
||||||
|
if (failedChapterNumberMatcher.find()) {
|
||||||
|
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user