mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-12 16:06:16 +00:00
Compare commits
348 Commits
v2.0.0
...
v2.2.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d778cd4350 | ||
|
|
83c07467a9 | ||
|
|
0225b28fea | ||
|
|
1e2a740dae | ||
|
|
22e687b9d8 | ||
|
|
f088b90964 | ||
|
|
ab199a3502 | ||
|
|
2493935349 | ||
|
|
c9699ba1fc | ||
|
|
051012be2d | ||
|
|
a38f1a2b2b | ||
|
|
0b66275995 | ||
|
|
7879fe4355 | ||
|
|
4aa88244ed | ||
|
|
bfe4d69b8a | ||
|
|
69fead70d1 | ||
|
|
92c663cd38 | ||
|
|
b829ed26f3 | ||
|
|
3d4834507d | ||
|
|
0758241e06 | ||
|
|
b093b5f979 | ||
|
|
d86481a0f7 | ||
|
|
de1788950f | ||
|
|
1e7b546b75 | ||
|
|
d53781e75a | ||
|
|
aa1830f12c | ||
|
|
17a87aa8c8 | ||
|
|
2662017fb7 | ||
|
|
f2b9cc3b3e | ||
|
|
2912ee5f73 | ||
|
|
9bb4c702f3 | ||
|
|
c4630f9243 | ||
|
|
57581d6f54 | ||
|
|
b5cfb5d567 | ||
|
|
04306a981f | ||
|
|
95409f7eda | ||
|
|
8741d820ad | ||
|
|
6020636a66 | ||
|
|
d15ffe7708 | ||
|
|
f77269e468 | ||
|
|
a2e44da99d | ||
|
|
8d7b86a667 | ||
|
|
0c6fad91ca | ||
|
|
c0f3fed142 | ||
|
|
21f5d503cd | ||
|
|
18b4f858d9 | ||
|
|
a101cac503 | ||
|
|
3f4c1953f8 | ||
|
|
a599b5b632 | ||
|
|
9fad3597bb | ||
|
|
f4b9889d67 | ||
|
|
d27b4f6905 | ||
|
|
855c7e623a | ||
|
|
a7e7bd0230 | ||
|
|
1d46897086 | ||
|
|
7af0721a75 | ||
|
|
884d738de9 | ||
|
|
ef71ca8a76 | ||
|
|
d4df12505b | ||
|
|
670d9b3913 | ||
|
|
19df5355ba | ||
|
|
8ca1c3be1f | ||
|
|
ed19aac553 | ||
|
|
7a67fbb980 | ||
|
|
d80b250650 | ||
|
|
300f2c2fb0 | ||
|
|
c2f108bf44 | ||
|
|
15abcd77d0 | ||
|
|
462f82e3fb | ||
|
|
eade3ce341 | ||
|
|
b4487159ed | ||
|
|
d12022266e | ||
|
|
13074a0f72 | ||
|
|
dc2c0c1027 | ||
|
|
97ed84127e | ||
|
|
d3f097f675 | ||
|
|
402e0576c8 | ||
|
|
aa8d41eecf | ||
|
|
54b53dbe56 | ||
|
|
2214c47c0c | ||
|
|
ed6275b0e8 | ||
|
|
97d062ffc2 | ||
|
|
c57c33c088 | ||
|
|
025d31102e | ||
|
|
cd96b6ab06 | ||
|
|
9358f86d43 | ||
|
|
7e51e067cd | ||
|
|
10ae66d1d0 | ||
|
|
cbdd1a2538 | ||
|
|
829292399b | ||
|
|
f7c7b8050f | ||
|
|
a3be59bd02 | ||
|
|
49e90a27b8 | ||
|
|
b559a13bab | ||
|
|
4d682f014d | ||
|
|
1a346e6b5a | ||
|
|
37a53748a2 | ||
|
|
9c2c932e75 | ||
|
|
c853d5bdf8 | ||
|
|
1f98e349dc | ||
|
|
883c14bf0d | ||
|
|
178abf0f83 | ||
|
|
c242fedfee | ||
|
|
922ccdfb27 | ||
|
|
965adddf8d | ||
|
|
8020b32541 | ||
|
|
33d798727c | ||
|
|
a0949c4e36 | ||
|
|
f00a0367cf | ||
|
|
eb5b83564f | ||
|
|
6b9dce10cf | ||
|
|
5d789bf96c | ||
|
|
17431734fb | ||
|
|
63c80fa526 | ||
|
|
2b79d437fc | ||
|
|
1e6041f99e | ||
|
|
fad4032a78 | ||
|
|
36ad006021 | ||
|
|
f6db690454 | ||
|
|
26575cfa0d | ||
|
|
73be639397 | ||
|
|
0ebd067bc2 | ||
|
|
49b3c33fbc | ||
|
|
4a5eab13c9 | ||
|
|
00ce6ce755 | ||
|
|
0aa95889aa | ||
|
|
3fdec074c6 | ||
|
|
29c6863b00 | ||
|
|
97eacb58a6 | ||
|
|
67bb28d027 | ||
|
|
513ed31b08 | ||
|
|
da5d95c7e5 | ||
|
|
7b9450807b | ||
|
|
8bc3631964 | ||
|
|
ab038983e5 | ||
|
|
79cff1ec9d | ||
|
|
4893cd0b03 | ||
|
|
b8fbeed785 | ||
|
|
8a9668bc79 | ||
|
|
d02d542207 | ||
|
|
4c797c5eb1 | ||
|
|
3fa2690277 | ||
|
|
67d482bad6 | ||
|
|
60981ba224 | ||
|
|
cb8ebfccb6 | ||
|
|
e5f0b71cf0 | ||
|
|
4218d81c49 | ||
|
|
9fa422ebf3 | ||
|
|
3ec488675f | ||
|
|
78b7d07500 | ||
|
|
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 |
129
.github/workflows/beta.yml
vendored
Normal file
129
.github/workflows/beta.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
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
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
- name: Download last SHA artifact
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: beta.yml
|
||||
name: last-sha
|
||||
path: .
|
||||
|
||||
continue-on-error: true
|
||||
|
||||
- name: Get Commits Since Last Run
|
||||
run: |
|
||||
if [ -f last_sha.txt ]; then
|
||||
LAST_SHA=$(cat last_sha.txt)
|
||||
else
|
||||
# Fallback to first commit if no previous SHA available
|
||||
LAST_SHA=$(git rev-list --max-parents=0 HEAD)
|
||||
fi
|
||||
echo "Commits since $LAST_SHA:"
|
||||
# Accumulate commit logs in a shell variable
|
||||
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"%h - %s")
|
||||
# URL-encode the newline characters for GitHub Actions
|
||||
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
|
||||
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
|
||||
COMMIT_LOGS="${COMMIT_LOGS//$'\r'/'%0D'}"
|
||||
# Append the encoded commit logs to the COMMIT_LOG environment variable
|
||||
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
|
||||
# Debugging: Print the variable to check its content
|
||||
echo "$COMMIT_LOGS"
|
||||
shell: /usr/bin/bash -e {0}
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Save Current SHA for Next Run
|
||||
run: echo ${{ github.sha }} > last_sha.txt
|
||||
|
||||
- name: Set variables
|
||||
run: |
|
||||
VER=$(grep -E -o "versionName \".*\"" app/build.gradle | sed -e 's/versionName //g' | tr -d '"')
|
||||
SHA=${{ github.sha }}
|
||||
VERSION="$VER+${SHA:0:7}"
|
||||
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 assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v3.0.0
|
||||
with:
|
||||
name: Dantotsu
|
||||
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
||||
|
||||
- name: Upload APK to Discord and Telegram
|
||||
shell: bash
|
||||
run: |
|
||||
#Discord
|
||||
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g')
|
||||
# Truncate commit messages if they are too long
|
||||
max_length=1900 # Adjust this value as needed
|
||||
if [ ${#commit_messages} -gt $max_length ]; then
|
||||
commit_messages="${commit_messages:0:$max_length}... (truncated)"
|
||||
fi
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
#Telegram
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
|
||||
-F "caption=[Alpha-Build: ${VERSION}] Change logs :${commit_messages}" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
|
||||
env:
|
||||
COMMIT_LOG: ${{ env.COMMIT_LOG }}
|
||||
VERSION: ${{ env.VERSION }}
|
||||
|
||||
- name: Upload Current SHA as Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: last-sha
|
||||
path: last_sha.txt
|
||||
|
||||
|
||||
- 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 }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,9 +23,6 @@ output.json
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
|
||||
<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://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>
|
||||
|
||||
# **Dantotsu** 🌟
|
||||
@@ -13,7 +14,7 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
|
||||
|
||||
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||
|
||||
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||
|
||||
@@ -31,6 +32,10 @@ You can come hang out with our awesome community, request new features, and repo
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## VISITORS
|
||||
|
||||
<img src="https://count.getloli.com/get/@:rebeloniondantotsu" alt=":rebeloniondantotsu" />
|
||||
|
||||
## LICENSE 📜
|
||||
|
||||
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
||||
|
||||
4
app/.gitignore
vendored
4
app/.gitignore
vendored
@@ -1,4 +1,8 @@
|
||||
/build
|
||||
/debug
|
||||
/debug/output-metadata.json
|
||||
/alpha
|
||||
/alpha/output-metadata.json
|
||||
/google/*
|
||||
/fdroid/*
|
||||
/release
|
||||
@@ -1,12 +1,9 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
id 'kotlin-android'
|
||||
id 'kotlinx-serialization'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'com.google.devtools.ksp'
|
||||
|
||||
}
|
||||
|
||||
def gitCommitHash = providers.exec {
|
||||
@@ -20,25 +17,51 @@ android {
|
||||
applicationId "ani.dantotsu"
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "2.0.0"
|
||||
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "2.2.0"
|
||||
versionCode 220000000
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
flavorDimensions "store"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
// F-Droid specific configuration
|
||||
dimension "store"
|
||||
versionNameSuffix "-fdroid"
|
||||
}
|
||||
google {
|
||||
// Google Play specific configuration
|
||||
dimension "store"
|
||||
isDefault true
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
alpha {
|
||||
applicationIdSuffix ".beta" // keep as beta by popular request
|
||||
versionNameSuffix "-alpha01"
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"]
|
||||
debuggable System.getenv("CI") == null
|
||||
isDefault true
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".beta"
|
||||
versionNameSuffix "-beta01"
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
|
||||
debuggable true
|
||||
debuggable false
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
|
||||
debuggable false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -52,22 +75,27 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// FireBase
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.1'
|
||||
// Core
|
||||
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.fragment:fragment-ktx:1.6.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
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-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.9.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.10.0'
|
||||
|
||||
// Glide
|
||||
ext.glide_version = '4.16.0'
|
||||
@@ -77,22 +105,20 @@ dependencies {
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// FireBase
|
||||
implementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.2.0'
|
||||
ext.exo_version = '1.2.1'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||
implementation "androidx.media3:media3-session:$exo_version"
|
||||
//media3 casting
|
||||
implementation "androidx.media3:media3-cast:$exo_version"
|
||||
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
||||
|
||||
// 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 'io.noties.markwon:core:4.6.2'
|
||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||
@@ -100,7 +126,7 @@ dependencies {
|
||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation "com.github.skydoves:colorpickerview:2.3.0"
|
||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||
|
||||
// string matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
@@ -112,16 +138,18 @@ dependencies {
|
||||
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
|
||||
implementation 'com.squareup.logcat:logcat:0.1'
|
||||
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||
implementation 'com.squareup.okio:okio:3.3.0'
|
||||
implementation 'ch.acra:acra-http:5.9.7'
|
||||
implementation 'com.squareup.okio:okio:3.7.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
|
||||
implementation 'ch.acra:acra-http:5.11.3'
|
||||
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.github.tachiyomiorg:unifile:17bec43'
|
||||
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'
|
||||
|
||||
}
|
||||
|
||||
86
app/google-services.json
Normal file
86
app/google-services.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"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.alpha"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
|
||||
"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"
|
||||
}
|
||||
376
app/src/alpha/res/drawable/anim_splash.xml
Normal file
376
app/src/alpha/res/drawable/anim_splash.xml
Normal file
@@ -0,0 +1,376 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="768dp"
|
||||
android:height="768dp"
|
||||
android:viewportWidth="768"
|
||||
android:viewportHeight="768">
|
||||
<group
|
||||
android:name="wrapper"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="clippath"
|
||||
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z" />
|
||||
<group android:name="group">
|
||||
<group android:name="group_1">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#ED0021"
|
||||
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
|
||||
android:strokeWidth="1" />
|
||||
<group
|
||||
android:name="group_12"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:fillColor="#D40037"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group android:name="group_2">
|
||||
<group android:name="group_7">
|
||||
<group android:name="group_10">
|
||||
<group
|
||||
android:name="group_11"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:fillColor="#A70060"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
<clip-path
|
||||
android:name="mask_2"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_13"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="mask_1"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z" />
|
||||
<group
|
||||
android:name="group_9"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:fillColor="#BF005E"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_6"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-5"
|
||||
android:scaleX="1.2"
|
||||
android:scaleY="1.2" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_8"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<group
|
||||
android:name="group_14"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440">
|
||||
<path
|
||||
android:name="path_4"
|
||||
android:fillColor="#C70051"
|
||||
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_3"
|
||||
android:translateX="-360">
|
||||
<path
|
||||
android:name="path_6"
|
||||
android:fillColor="#251528"
|
||||
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_4"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384"
|
||||
android:scaleX="1.5"
|
||||
android:scaleY="1.5">
|
||||
<path
|
||||
android:name="path_5"
|
||||
android:fillColor="#251528"
|
||||
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_5"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384"
|
||||
android:rotation="-15"
|
||||
android:scaleX="3"
|
||||
android:scaleY="3">
|
||||
<path
|
||||
android:name="path_7"
|
||||
android:fillAlpha="0"
|
||||
android:fillColor="#FFD8DF"
|
||||
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="wrapper">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_6">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="-10"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_3">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="400"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="translateX"
|
||||
android:startOffset="250"
|
||||
android:valueFrom="-360"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_4">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="400"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="400"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_7">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_5">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="-45"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_8">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_9">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_11">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_12">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_13">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_14">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="200"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="5"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="250"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="5"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
4
app/src/alpha/res/values/strings.xml
Normal file
4
app/src/alpha/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dantotsu α</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,4 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
@@ -14,23 +13,23 @@
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="clippath"
|
||||
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z"/>
|
||||
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z" />
|
||||
<group android:name="group">
|
||||
<group android:name="group_1">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
|
||||
android:fillColor="#6901fd"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
|
||||
android:strokeWidth="1" />
|
||||
<group
|
||||
android:name="group_12"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
|
||||
android:fillColor="#4800e5"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group android:name="group_2">
|
||||
@@ -43,12 +42,12 @@
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:fillColor="#2000bd"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
<clip-path
|
||||
android:name="mask_2"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"/>
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
@@ -57,7 +56,7 @@
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="mask_1"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"/>
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z" />
|
||||
<group
|
||||
android:name="group_9"
|
||||
android:pivotX="94"
|
||||
@@ -65,18 +64,18 @@
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:fillColor="#1e00d1"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_6"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-5"
|
||||
android:scaleX="1.2"
|
||||
android:scaleY="1.2"
|
||||
android:rotation="-5"/>
|
||||
android:scaleY="1.2" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_8"
|
||||
@@ -89,9 +88,9 @@
|
||||
android:pivotY="440">
|
||||
<path
|
||||
android:name="path_4"
|
||||
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
|
||||
android:fillColor="#2900da"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
@@ -100,9 +99,9 @@
|
||||
android:translateX="-360">
|
||||
<path
|
||||
android:name="path_6"
|
||||
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
|
||||
android:fillColor="#1f1f30"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_4"
|
||||
@@ -112,23 +111,23 @@
|
||||
android:scaleY="1.5">
|
||||
<path
|
||||
android:name="path_5"
|
||||
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
|
||||
android:fillColor="#1f1f30"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_5"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384"
|
||||
android:rotation="-15"
|
||||
android:scaleX="3"
|
||||
android:scaleY="3"
|
||||
android:rotation="-15">
|
||||
android:scaleY="3">
|
||||
<path
|
||||
android:name="path_7"
|
||||
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
|
||||
android:fillColor="#efe7ff"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1"/>
|
||||
android:fillColor="#efe7ff"
|
||||
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
@@ -138,19 +137,19 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:duration="500"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
@@ -158,177 +157,177 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="-10"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:duration="300"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:duration="300"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_3">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="400"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="translateX"
|
||||
android:startOffset="250"
|
||||
android:duration="400"
|
||||
android:valueFrom="-360"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_4">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="400"
|
||||
android:duration="350"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="400"
|
||||
android:duration="350"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_7">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_5">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="-45"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_8">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:duration="350"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_9">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:duration="350"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:duration="350"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_11">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:duration="350"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_12">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:duration="550"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
@@ -336,19 +335,19 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:duration="550"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
@@ -356,21 +355,21 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="200"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:duration="200"
|
||||
android:valueFrom="5"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="250"
|
||||
android:duration="100"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="5"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dantotsu ß</string>
|
||||
<string name="app_name">Dantotsu β</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
class CrashlyticsFactory {
|
||||
companion object {
|
||||
fun createCrashlytics(): CrashlyticsInterface {
|
||||
return CrashlyticsStub()
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
Normal file
9
app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.others
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
class CrashlyticsFactory {
|
||||
companion object {
|
||||
fun createCrashlytics(): CrashlyticsInterface {
|
||||
return FirebaseCrashlytics()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.FirebaseApp
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.google.firebase.ktx.app
|
||||
|
||||
class FirebaseCrashlytics : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
FirebaseApp.initializeApp(context)
|
||||
}
|
||||
override fun logException(e: Throwable) {
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
|
||||
override fun log(message: String) {
|
||||
FirebaseCrashlytics.getInstance().log(message)
|
||||
}
|
||||
|
||||
override fun setUserId(id: String) {
|
||||
Firebase.crashlytics.setUserId(id)
|
||||
}
|
||||
|
||||
override fun setCustomKey(key: String, value: String) {
|
||||
FirebaseCrashlytics.getInstance().setCustomKey(key, value)
|
||||
}
|
||||
|
||||
override fun setCrashlyticsCollectionEnabled(enabled: Boolean) {
|
||||
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -38,7 +39,7 @@ object AppUpdater {
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
val r = res.filter { it.prerelease }.maxByOrNull {
|
||||
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
@@ -50,7 +51,7 @@ object AppUpdater {
|
||||
}
|
||||
|
||||
logger("Git Version : $version")
|
||||
val dontShow = loadData("dont_ask_for_update_$version") ?: false
|
||||
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
|
||||
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
setTitleText(
|
||||
@@ -71,7 +72,7 @@ object AppUpdater {
|
||||
false
|
||||
) { isChecked ->
|
||||
if (isChecked) {
|
||||
saveData("dont_ask_for_update_$version", true)
|
||||
PrefManager.setCustomVal("dont_ask_for_update_$version", true)
|
||||
}
|
||||
}
|
||||
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||
@@ -186,7 +187,7 @@ object AppUpdater {
|
||||
return true
|
||||
}
|
||||
|
||||
fun openApk(context: Context, uri: Uri) {
|
||||
private fun openApk(context: Context, uri: Uri) {
|
||||
try {
|
||||
uri.path?.let {
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
@@ -10,6 +10,9 @@
|
||||
android:required="false" />
|
||||
|
||||
<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.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
@@ -17,25 +20,22 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||
<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.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<queries>
|
||||
@@ -48,6 +48,7 @@
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/ic_banner_foreground"
|
||||
android:icon="${icon_placeholder}"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
@@ -56,14 +57,28 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Dantotsu"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup"
|
||||
android:banner="@drawable/ic_banner_foreground">
|
||||
tools:ignore="AllowBackup">
|
||||
<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
|
||||
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity"
|
||||
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" >
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/epub+zip" />
|
||||
@@ -71,13 +86,11 @@
|
||||
<data android:mimeType="application/vnd.amazon.ebook" />
|
||||
<data android:mimeType="application/fb2+zip" />
|
||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||
|
||||
<data android:pathPattern=".*\\.epub" />
|
||||
<data android:pathPattern=".*\\.mobi" />
|
||||
<data android:pathPattern=".*\\.kf8" />
|
||||
<data android:pathPattern=".*\\.fb2" />
|
||||
<data android:pathPattern=".*\\.cbz" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
@@ -103,9 +116,9 @@
|
||||
<activity
|
||||
android:name=".media.CalendarActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name="ani.dantotsu.media.user.ListActivity" />
|
||||
<activity android:name=".media.user.ListActivity" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity"
|
||||
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="@string/manga"
|
||||
@@ -118,7 +131,7 @@
|
||||
<activity android:name=".media.CharacterDetailsActivity" />
|
||||
<activity android:name=".home.NoInternet" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.anime.ExoplayerView"
|
||||
android:name=".media.anime.ExoplayerView"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -127,7 +140,7 @@
|
||||
android:supportsPictureInPicture="true"
|
||||
tools:targetApi="n" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.connections.anilist.Login"
|
||||
android:name=".connections.anilist.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -144,7 +157,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="ani.dantotsu.connections.mal.Login"
|
||||
android:name=".connections.mal.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -160,8 +173,8 @@
|
||||
android:scheme="dantotsu" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="ani.dantotsu.connections.discord.Login"
|
||||
<activity
|
||||
android:name=".connections.discord.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -172,15 +185,32 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="dantotsu"/>
|
||||
<data android:scheme="dantotsu" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="discord.dantotsu.com"/>
|
||||
<data android:host="discord.dantotsu.com" />
|
||||
</intent-filter>
|
||||
</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:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -216,30 +246,30 @@
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".download.DownloadContainerActivity" />
|
||||
<activity
|
||||
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
|
||||
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
|
||||
android:name=".subcriptions.AlarmReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="Aani.dantotsu.ACTION_ALARM"/>
|
||||
<action android:name="Aani.dantotsu.ACTION_ALARM" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
@@ -258,32 +288,52 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<service android:name=".download.video.MyDownloadService"
|
||||
android:exported="false">
|
||||
<service
|
||||
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name=".download.video.ExoplayerDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".download.manga.MangaDownloaderService"
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name=".download.novel.NovelDownloaderService"
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name=".connections.discord.DiscordService"
|
||||
<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:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher_alpha-playstore.png
Normal file
BIN
app/src/main/ic_launcher_alpha-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -8,14 +8,16 @@ import androidx.multidex.MultiDex
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.others.DisabledReports
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
@@ -49,18 +51,35 @@ class App : MultiDexApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
|
||||
|
||||
PrefManager.init(this)
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(PreferenceModule(this))
|
||||
|
||||
val crashlytics = Injekt.get<CrashlyticsInterface>()
|
||||
crashlytics.initialize(this)
|
||||
|
||||
val useMaterialYou: Boolean = PrefManager.getVal(PrefName.UseMaterialYou)
|
||||
if (useMaterialYou) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
//TODO: HarmonizedColors
|
||||
}
|
||||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||
|
||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
(PrefManager.getVal(PrefName.SharedUserID) as Boolean).let {
|
||||
if (!it) return@let
|
||||
val dUsername = PrefManager.getVal(PrefName.DiscordUserName, null as String?)
|
||||
val aUsername = PrefManager.getVal(PrefName.AnilistUserName, null as String?)
|
||||
if (dUsername != null) {
|
||||
crashlytics.setCustomKey("dUsername", dUsername)
|
||||
}
|
||||
if (aUsername != null) {
|
||||
crashlytics.setCustomKey("aUsername", aUsername)
|
||||
}
|
||||
}
|
||||
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
||||
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(PreferenceModule(this))
|
||||
|
||||
initializeNetwork(baseContext)
|
||||
|
||||
@@ -91,7 +110,6 @@ class App : MultiDexApplication() {
|
||||
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,15 +4,19 @@ import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources.getSystem
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.Manifest
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities.*
|
||||
@@ -23,11 +27,16 @@ import android.telephony.TelephonyManager
|
||||
import android.text.InputFilter
|
||||
import android.text.Spanned
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.animation.*
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
@@ -40,10 +49,15 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||
import ani.dantotsu.connections.anilist.Genre
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||
import ani.dantotsu.subcriptions.NotificationClickReceiver
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
||||
@@ -53,8 +67,11 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.internal.ViewUtils
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import kotlinx.coroutines.*
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.*
|
||||
import java.lang.Runnable
|
||||
import java.lang.reflect.Field
|
||||
@@ -96,56 +113,22 @@ fun logger(e: Any?, print: Boolean = true) {
|
||||
println(e)
|
||||
}
|
||||
|
||||
fun saveData(fileName: String, data: Any?, context: Context? = null) {
|
||||
tryWith {
|
||||
val a = context ?: currContext()
|
||||
if (a != null) {
|
||||
val fos: FileOutputStream = a.openFileOutput(fileName, Context.MODE_PRIVATE)
|
||||
val os = ObjectOutputStream(fos)
|
||||
os.writeObject(data)
|
||||
os.close()
|
||||
fos.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = true): T? {
|
||||
val a = context ?: currContext()
|
||||
try {
|
||||
if (a?.fileList() != null)
|
||||
if (fileName in a.fileList()) {
|
||||
val fileIS: FileInputStream = a.openFileInput(fileName)
|
||||
val objIS = ObjectInputStream(fileIS)
|
||||
val data = objIS.readObject() as T
|
||||
objIS.close()
|
||||
fileIS.close()
|
||||
return data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun initActivity(a: Activity) {
|
||||
val window = a.window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false)
|
||||
?: UserInterfaceSettings().apply {
|
||||
saveData("ui_settings", this)
|
||||
}
|
||||
uiSettings.darkMode.apply {
|
||||
val darkMode = PrefManager.getVal<Int>(PrefName.DarkMode)
|
||||
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
||||
darkMode.apply {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (this) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
2 -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
1 -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
}
|
||||
if (uiSettings.immersiveMode) {
|
||||
if (immersiveMode) {
|
||||
if (navBarHeight == 0) {
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||
?.apply {
|
||||
@@ -198,6 +181,14 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
val typedValue = TypedValue()
|
||||
val theme = requireContext().theme
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSurface,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
window.navigationBarColor = typedValue.data
|
||||
}
|
||||
|
||||
override fun show(manager: FragmentManager, tag: String?) {
|
||||
@@ -305,20 +296,20 @@ class InputFilterMinMax(
|
||||
}
|
||||
|
||||
|
||||
class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) :
|
||||
class ZoomOutPageTransformer() :
|
||||
ViewPager2.PageTransformer {
|
||||
override fun transformPage(view: View, position: Float) {
|
||||
if (position == 0.0f && uiSettings.layoutAnimations) {
|
||||
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
setAnimation(
|
||||
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()
|
||||
.setDuration((200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong())
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,12 +317,11 @@ class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) :
|
||||
fun setAnimation(
|
||||
context: Context,
|
||||
viewToAnimate: View,
|
||||
uiSettings: UserInterfaceSettings,
|
||||
duration: Long = 150,
|
||||
list: FloatArray = floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f),
|
||||
pivot: Pair<Float, Float> = 0.5f to 0.5f
|
||||
) {
|
||||
if (uiSettings.layoutAnimations) {
|
||||
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
val anim = ScaleAnimation(
|
||||
list[0],
|
||||
list[1],
|
||||
@@ -342,7 +332,7 @@ fun setAnimation(
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
pivot.second
|
||||
)
|
||||
anim.duration = (duration * uiSettings.animationSpeed).toLong()
|
||||
anim.duration = (duration * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
|
||||
anim.setInterpolator(context, R.anim.over_shoot)
|
||||
viewToAnimate.startAnimation(anim)
|
||||
}
|
||||
@@ -595,18 +585,117 @@ fun openLinkInBrowser(link: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
|
||||
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"$APPLICATION_ID.provider",
|
||||
saveImage(
|
||||
bitmap,
|
||||
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS,
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||
title
|
||||
) ?: return
|
||||
)
|
||||
}
|
||||
|
||||
fun savePrefsToDownloads(
|
||||
title: String,
|
||||
serialized: String,
|
||||
context: Activity,
|
||||
password: CharArray? = null
|
||||
) {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"$APPLICATION_ID.provider",
|
||||
if (password != null) {
|
||||
savePrefs(
|
||||
serialized,
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||
title,
|
||||
context,
|
||||
password
|
||||
) ?: return
|
||||
} else {
|
||||
savePrefs(
|
||||
serialized,
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||
title,
|
||||
context
|
||||
) ?: return
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
|
||||
var file = File(path, "$title.ani")
|
||||
var counter = 1
|
||||
while (file.exists()) {
|
||||
file = File(path, "${title}_${counter}.ani")
|
||||
counter++
|
||||
}
|
||||
|
||||
return try {
|
||||
file.writeText(serialized)
|
||||
scanFile(file.absolutePath, context)
|
||||
toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath)))
|
||||
file
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to save settings: ${e.localizedMessage}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun savePrefs(
|
||||
serialized: String,
|
||||
path: String,
|
||||
title: String,
|
||||
context: Context,
|
||||
password: CharArray
|
||||
): File? {
|
||||
var file = File(path, "$title.sani")
|
||||
var counter = 1
|
||||
while (file.exists()) {
|
||||
file = File(path, "${title}_${counter}.sani")
|
||||
counter++
|
||||
}
|
||||
|
||||
val salt = generateSalt()
|
||||
|
||||
return try {
|
||||
val encryptedData = PreferenceKeystore.encryptWithPassword(password, serialized, salt)
|
||||
|
||||
// Combine salt and encrypted data
|
||||
val dataToSave = salt + encryptedData
|
||||
|
||||
file.writeBytes(dataToSave)
|
||||
scanFile(file.absolutePath, context)
|
||||
toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath)))
|
||||
file
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to save settings: ${e.localizedMessage}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
val requiredPermissions = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
||||
}.toTypedArray()
|
||||
|
||||
return if (requiredPermissions.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
|
||||
|
||||
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
@@ -624,13 +713,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||
|
||||
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
||||
val imageFile = File(path, "$imageFileName.png")
|
||||
return tryWith {
|
||||
return try {
|
||||
val fOut: OutputStream = FileOutputStream(imageFile)
|
||||
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
|
||||
fOut.close()
|
||||
scanFile(imageFile.absolutePath, currContext()!!)
|
||||
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
|
||||
imageFile
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to save image: ${e.localizedMessage}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,7 +765,7 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
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)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
@@ -720,10 +812,11 @@ fun MutableMap<String, Genre>.checkGenreTime(genre: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun setSlideIn(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
||||
if (uiSettings.layoutAnimations) {
|
||||
fun setSlideIn() = AnimationSet(false).apply {
|
||||
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
var animation: Animation = AlphaAnimation(0.0f, 1.0f)
|
||||
animation.duration = (500 * uiSettings.animationSpeed).toLong()
|
||||
val animationSpeed: Float = PrefManager.getVal(PrefName.AnimationSpeed)
|
||||
animation.duration = (500 * animationSpeed).toLong()
|
||||
animation.interpolator = AccelerateDecelerateInterpolator()
|
||||
addAnimation(animation)
|
||||
|
||||
@@ -734,16 +827,17 @@ fun setSlideIn(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
||||
Animation.RELATIVE_TO_SELF, 0f
|
||||
)
|
||||
|
||||
animation.duration = (750 * uiSettings.animationSpeed).toLong()
|
||||
animation.duration = (750 * animationSpeed).toLong()
|
||||
animation.interpolator = OvershootInterpolator(1.1f)
|
||||
addAnimation(animation)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSlideUp(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
||||
if (uiSettings.layoutAnimations) {
|
||||
fun setSlideUp() = AnimationSet(false).apply {
|
||||
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
var animation: Animation = AlphaAnimation(0.0f, 1.0f)
|
||||
animation.duration = (500 * uiSettings.animationSpeed).toLong()
|
||||
val animationSpeed: Float = PrefManager.getVal(PrefName.AnimationSpeed)
|
||||
animation.duration = (500 * animationSpeed).toLong()
|
||||
animation.interpolator = AccelerateDecelerateInterpolator()
|
||||
addAnimation(animation)
|
||||
|
||||
@@ -754,7 +848,7 @@ fun setSlideUp(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
||||
Animation.RELATIVE_TO_SELF, 0f
|
||||
)
|
||||
|
||||
animation.duration = (750 * uiSettings.animationSpeed).toLong()
|
||||
animation.duration = (750 * animationSpeed).toLong()
|
||||
animation.interpolator = OvershootInterpolator(1.1f)
|
||||
addAnimation(animation)
|
||||
}
|
||||
@@ -783,35 +877,40 @@ fun toast(string: String?) {
|
||||
}
|
||||
|
||||
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(
|
||||
window.decorView.findViewById(android.R.id.content),
|
||||
s,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
width = WRAP_CONTENT
|
||||
}
|
||||
translationY = -(navBarHeight.dp + 32f)
|
||||
translationZ = 32f
|
||||
updatePadding(16f.px, right = 16f.px)
|
||||
setOnClickListener {
|
||||
snackBar.dismiss()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
copyToClipboard(clipboard ?: s, false)
|
||||
toast(getString(R.string.copied_to_clipboard))
|
||||
true
|
||||
try { //I have no idea why this sometimes crashes for some people...
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(
|
||||
window.decorView.findViewById(android.R.id.content),
|
||||
s,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
width = WRAP_CONTENT
|
||||
}
|
||||
translationY = -(navBarHeight.dp + 32f)
|
||||
translationZ = 32f
|
||||
updatePadding(16f.px, right = 16f.px)
|
||||
setOnClickListener {
|
||||
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())
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -930,6 +1029,32 @@ fun checkCountry(context: Context): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
const val INCOGNITO_CHANNEL_ID = 26
|
||||
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
fun incognitoNotification(context: Context) {
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if (incognito) {
|
||||
val intent = Intent(context, NotificationClickReceiver::class.java)
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context, 0, intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
val builder = NotificationCompat.Builder(context, Notifications.CHANNEL_INCOGNITO_MODE)
|
||||
.setSmallIcon(R.drawable.ic_incognito_24)
|
||||
.setContentTitle("Incognito Mode")
|
||||
.setContentText("Disable Incognito Mode")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
notificationManager.notify(INCOGNITO_CHANNEL_ID, builder.build())
|
||||
} else {
|
||||
notificationManager.cancel(INCOGNITO_CHANNEL_ID)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun View.pop() {
|
||||
currActivity()?.runOnUiThread {
|
||||
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ani.dantotsu
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
@@ -11,12 +11,15 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -26,11 +29,14 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
@@ -38,33 +44,43 @@ import ani.dantotsu.home.MangaFragment
|
||||
import ani.dantotsu.home.NoInternet
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.Serializable
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
|
||||
private val scope = lifecycleScope
|
||||
private var load = false
|
||||
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
|
||||
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
//get FRAGMENT_CLASS_NAME from intent
|
||||
val fragment = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -73,17 +89,54 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
}
|
||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
|
||||
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 = PrefManager.getLiveVal(
|
||||
PrefName.Incognito,
|
||||
false
|
||||
).asLiveBool()
|
||||
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
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
@@ -98,6 +151,17 @@ 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
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -141,107 +205,146 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
selectedOption = if (fragment != null) {
|
||||
when (fragment) {
|
||||
AnimeFragment::class.java.name -> 0
|
||||
HomeFragment::class.java.name -> 1
|
||||
MangaFragment::class.java.name -> 2
|
||||
else -> 1
|
||||
}
|
||||
} else {
|
||||
PrefManager.getVal(PrefName.DefaultStartUpTab)
|
||||
}
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
if (!isOnline(this)) {
|
||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load) {
|
||||
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"))
|
||||
if (offlineMode) {
|
||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) { it ->
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
selectedOption,
|
||||
false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
}
|
||||
}.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 (!(PrefManager.getVal(PrefName.AllowOpeningLinks) as Boolean)) {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
||||
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
||||
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)) {
|
||||
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
||||
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
|
||||
tryWith(true) {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: Remove this
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||
val downloadCursor = index.getDownloads()
|
||||
while (downloadCursor.moveToNext()) {
|
||||
val download = downloadCursor.download
|
||||
Log.e("Downloader", download.request.uri.toString())
|
||||
Log.e("Downloader", download.request.id)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -261,4 +364,4 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ package ani.dantotsu.aniyomi.anime.custom
|
||||
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
@@ -13,7 +16,6 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -27,12 +29,13 @@ import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { DownloadsManager(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app, get()) }
|
||||
addSingletonFactory { NetworkHelper(app) }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
addSingletonFactory { MangaExtensionManager(app) }
|
||||
@@ -41,9 +44,6 @@ class AppModule(val app: Application) : InjektModule {
|
||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||
|
||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
addSingleton(sharedPreferences)
|
||||
|
||||
addSingletonFactory {
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
@@ -51,6 +51,12 @@ class AppModule(val app: Application) : InjektModule {
|
||||
}
|
||||
}
|
||||
|
||||
addSingletonFactory { StandaloneDatabaseProvider(app) }
|
||||
|
||||
addSingletonFactory<CrashlyticsInterface> {
|
||||
ani.dantotsu.connections.crashlytics.CrashlyticsFactory.createCrashlytics()
|
||||
}
|
||||
|
||||
addSingletonFactory { MangaCache() }
|
||||
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
@@ -66,13 +72,6 @@ class PreferenceModule(val application: Application) : InjektModule {
|
||||
AndroidPreferenceStore(application)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
NetworkPreferences(
|
||||
preferenceStore = get(),
|
||||
verboseLogging = false,
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
SourcePreferences(get())
|
||||
}
|
||||
|
||||
@@ -6,15 +6,15 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun updateProgress(media: Media, number: String) {
|
||||
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
|
||||
?.getBoolean("incognito", false) ?: false
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if (!incognito) {
|
||||
if (Anilist.userid != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
@@ -3,13 +3,16 @@ package ani.dantotsu.connections.anilist
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
|
||||
object Anilist {
|
||||
@@ -28,6 +31,8 @@ object Anilist {
|
||||
var genres: ArrayList<String>? = null
|
||||
var tags: Map<Boolean, List<String>>? = null
|
||||
|
||||
var rateLimitReset: Long = 0
|
||||
|
||||
val sortBy = listOf(
|
||||
"SCORE_DESC",
|
||||
"POPULARITY_DESC",
|
||||
@@ -94,15 +99,12 @@ object Anilist {
|
||||
}
|
||||
}
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
if ("anilistToken" in context.fileList()) {
|
||||
token = File(context.filesDir, "anilistToken").readText()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
fun getSavedToken(): Boolean {
|
||||
token = PrefManager.getVal(PrefName.AnilistToken, null as String?)
|
||||
return !token.isNullOrEmpty()
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
fun removeSavedToken() {
|
||||
token = null
|
||||
username = null
|
||||
adult = false
|
||||
@@ -111,9 +113,7 @@ object Anilist {
|
||||
bg = null
|
||||
episodesWatched = null
|
||||
chapterRead = null
|
||||
if ("anilistToken" in context.fileList()) {
|
||||
File(context.filesDir, "anilistToken").delete()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.AnilistToken)
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> executeQuery(
|
||||
@@ -125,6 +125,10 @@ object Anilist {
|
||||
cache: Int? = null
|
||||
): T? {
|
||||
return tryWithSuspend {
|
||||
if (rateLimitReset > System.currentTimeMillis() / 1000) {
|
||||
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||
}
|
||||
val data = mapOf(
|
||||
"query" to query,
|
||||
"variables" to variables
|
||||
@@ -143,6 +147,18 @@ object Anilist {
|
||||
data = data,
|
||||
cacheTime = cache ?: 10
|
||||
)
|
||||
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
|
||||
Log.d("AnilistQuery", "Remaining requests: $remaining")
|
||||
if (json.code == 429) {
|
||||
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
|
||||
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
|
||||
if (retry > 0) {
|
||||
rateLimitReset = passedLimitReset
|
||||
}
|
||||
|
||||
toast("Rate limited. Try after $retry seconds")
|
||||
throw Exception("Rate limited after $retry seconds")
|
||||
}
|
||||
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
if (show) println("Response : ${json.text}")
|
||||
json.parsed()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Base64
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.checkGenreTime
|
||||
import ani.dantotsu.checkId
|
||||
@@ -10,18 +10,24 @@ import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.Page
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Author
|
||||
import ani.dantotsu.media.Character
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.Studio
|
||||
import ani.dantotsu.others.MalScraper
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class AnilistQueries {
|
||||
@@ -33,6 +39,8 @@ class AnilistQueries {
|
||||
}.also { println("time : $it") }
|
||||
val user = response?.data?.user ?: return false
|
||||
|
||||
PrefManager.setVal(PrefName.AnilistUserName, user.name)
|
||||
|
||||
Anilist.userid = user.id
|
||||
Anilist.username = user.name
|
||||
Anilist.bg = user.bannerImage
|
||||
@@ -239,7 +247,9 @@ class AnilistQueries {
|
||||
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
||||
}
|
||||
} 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 {
|
||||
@@ -255,12 +265,33 @@ class AnilistQueries {
|
||||
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
val statuses = if (!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING")
|
||||
suspend fun repeat(status: String) {
|
||||
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 } } } } } } """)
|
||||
val query = if (planned) {
|
||||
"""{ planned: ${continueMediaQuery(type, "PLANNING")} }"""
|
||||
} else {
|
||||
"""{
|
||||
current: ${continueMediaQuery(type, "CURRENT")},
|
||||
repeating: ${continueMediaQuery(type, "REPEATING")}
|
||||
}"""
|
||||
}
|
||||
|
||||
response?.data?.mediaListCollection?.lists?.forEach { li ->
|
||||
val response = executeQuery<Query.CombinedMediaListResponse>(query)
|
||||
if (planned) {
|
||||
response?.data?.planned?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response?.data?.current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
response?.data?.repeating?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
@@ -268,10 +299,8 @@ class AnilistQueries {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statuses.forEach { repeat(it) }
|
||||
val set = loadData<MutableSet<Int>>("continue_$type")
|
||||
if (set != null) {
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
if (map.containsKey(it)) returnArray.add(map[it]!!)
|
||||
}
|
||||
@@ -282,13 +311,16 @@ class AnilistQueries {
|
||||
return returnArray
|
||||
}
|
||||
|
||||
private fun continueMediaQuery(type: String, status: String): String {
|
||||
return """ 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 } } } } } """
|
||||
}
|
||||
|
||||
suspend fun favMedia(anime: Boolean): ArrayList<Media> {
|
||||
var hasNextPage = true
|
||||
var page = 0
|
||||
|
||||
suspend fun getNextPage(page: Int): List<Media> {
|
||||
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}}}}}}}""")
|
||||
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page)}}""")
|
||||
val favourites = response?.data?.user?.favourites
|
||||
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
||||
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
||||
@@ -307,9 +339,12 @@ class AnilistQueries {
|
||||
return responseArray
|
||||
}
|
||||
|
||||
private fun favMediaQuery(anime: Boolean, page: Int): String {
|
||||
return """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}}}}}}"""
|
||||
}
|
||||
|
||||
suspend fun recommendations(): ArrayList<Media> {
|
||||
val response =
|
||||
executeQuery<Query.Page>(""" { Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } } """)
|
||||
val response = executeQuery<Query.Page>("""{${recommendationQuery()}}""")
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
response?.data?.page?.apply {
|
||||
recommendations?.onEach {
|
||||
@@ -325,7 +360,7 @@ class AnilistQueries {
|
||||
val types = arrayOf("ANIME", "MANGA")
|
||||
suspend fun repeat(type: String) {
|
||||
val res =
|
||||
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """)
|
||||
executeQuery<Query.MediaListCollection>("""{${recommendationPlannedQuery(type)}}""")
|
||||
res?.data?.mediaListCollection?.lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
@@ -343,8 +378,191 @@ class AnilistQueries {
|
||||
return list
|
||||
}
|
||||
|
||||
private fun recommendationQuery(): String {
|
||||
return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """
|
||||
}
|
||||
|
||||
private fun recommendationPlannedQuery(type: String): String {
|
||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
|
||||
}
|
||||
|
||||
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
|
||||
val toShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayoutShow) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations
|
||||
var query = """{"""
|
||||
if (toShow.getOrNull(0) == true) query += """currentAnime: ${
|
||||
continueMediaQuery(
|
||||
"ANIME",
|
||||
"CURRENT"
|
||||
)
|
||||
}, repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}"""
|
||||
if (toShow.getOrNull(1) == true) query += """favoriteAnime: ${favMediaQuery(true, 1)}"""
|
||||
if (toShow.getOrNull(2) == true) query += """plannedAnime: ${
|
||||
continueMediaQuery(
|
||||
"ANIME",
|
||||
"PLANNING"
|
||||
)
|
||||
}"""
|
||||
if (toShow.getOrNull(3) == true) query += """currentManga: ${
|
||||
continueMediaQuery(
|
||||
"MANGA",
|
||||
"CURRENT"
|
||||
)
|
||||
}, repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}"""
|
||||
if (toShow.getOrNull(4) == true) query += """favoriteManga: ${favMediaQuery(false, 1)}"""
|
||||
if (toShow.getOrNull(5) == true) query += """plannedManga: ${
|
||||
continueMediaQuery(
|
||||
"MANGA",
|
||||
"PLANNING"
|
||||
)
|
||||
}"""
|
||||
if (toShow.getOrNull(6) == true) query += """recommendationQuery: ${recommendationQuery()}, recommendationPlannedQueryAnime: ${
|
||||
recommendationPlannedQuery(
|
||||
"ANIME"
|
||||
)
|
||||
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
|
||||
query += """}""".trimEnd(',')
|
||||
|
||||
val response = executeQuery<Query.HomePageMedia>(query)
|
||||
val returnMap = mutableMapOf<String, ArrayList<Media>>()
|
||||
fun current(type: String) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val current =
|
||||
if (type == "Anime") response?.data?.currentAnime else response?.data?.currentManga
|
||||
val repeating =
|
||||
if (type == "Anime") response?.data?.repeatingAnime else response?.data?.repeatingManga
|
||||
current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
repeating?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_${type.uppercase()}", setOf())
|
||||
.toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
|
||||
}
|
||||
for (i in subMap) {
|
||||
if (i.value !in returnArray) returnArray.add(i.value)
|
||||
}
|
||||
} else returnArray.addAll(subMap.values)
|
||||
returnMap["current$type"] = returnArray
|
||||
|
||||
}
|
||||
|
||||
fun planned(type: String) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val current =
|
||||
if (type == "Anime") response?.data?.plannedAnime else response?.data?.plannedManga
|
||||
current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
|
||||
}
|
||||
for (i in subMap) {
|
||||
if (i.value !in returnArray) returnArray.add(i.value)
|
||||
}
|
||||
} else returnArray.addAll(subMap.values)
|
||||
returnMap["planned$type"] = returnArray
|
||||
}
|
||||
|
||||
fun favorite(type: String) {
|
||||
val favourites =
|
||||
if (type == "Anime") response?.data?.favoriteAnime?.favourites else response?.data?.favoriteManga?.favourites
|
||||
val apiMediaList = if (type == "Anime") favourites?.anime else favourites?.manga
|
||||
val returnArray = arrayListOf<Media>()
|
||||
apiMediaList?.edges?.forEach {
|
||||
it.node?.let { i ->
|
||||
returnArray.add(Media(i).apply { isFav = true })
|
||||
}
|
||||
}
|
||||
returnMap["favorite$type"] = returnArray
|
||||
}
|
||||
|
||||
if (toShow.getOrNull(0) == true) {
|
||||
current("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(1) == true) {
|
||||
favorite("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(2) == true) {
|
||||
planned("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(3) == true) {
|
||||
current("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(4) == true) {
|
||||
favorite("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(5) == true) {
|
||||
planned("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(6) == true) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
response?.data?.recommendationQuery?.apply {
|
||||
recommendations?.onEach {
|
||||
val json = it.mediaRecommendation
|
||||
if (json != null) {
|
||||
val m = Media(json)
|
||||
m.relation = json.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
response?.data?.recommendationPlannedQueryAnime?.apply {
|
||||
lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.status == "RELEASING" || m.status == "FINISHED") {
|
||||
m.relation = it.media?.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response?.data?.recommendationPlannedQueryManga?.apply {
|
||||
lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.status == "RELEASING" || m.status == "FINISHED") {
|
||||
m.relation = it.media?.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val list = ArrayList(subMap.values.toList())
|
||||
list.sortByDescending { it.meanScore }
|
||||
returnMap["recommendations"] = list
|
||||
}
|
||||
return returnMap
|
||||
}
|
||||
|
||||
|
||||
private suspend fun bannerImage(type: String): String? {
|
||||
var image = loadData<BannerImage>("banner_$type")
|
||||
//var image = loadData<BannerImage>("banner_$type")
|
||||
val image: BannerImage? = BannerImage(
|
||||
PrefManager.getCustomVal("banner_${type}_url", null),
|
||||
PrefManager.getCustomVal("banner_${type}_time", 0L)
|
||||
)
|
||||
if (image == null || image.checkTime()) {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
|
||||
@@ -355,13 +573,9 @@ class AnilistQueries {
|
||||
else null
|
||||
}
|
||||
}?.flatten()?.randomOrNull() ?: return null
|
||||
|
||||
image = BannerImage(
|
||||
random,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
saveData("banner_$type", image)
|
||||
return image.url
|
||||
PrefManager.setCustomVal("banner_${type}_url", random)
|
||||
PrefManager.setCustomVal("banner_${type}_time", System.currentTimeMillis())
|
||||
return random
|
||||
} else return image.url
|
||||
}
|
||||
|
||||
@@ -408,10 +622,17 @@ class AnilistQueries {
|
||||
|
||||
sorted["Favourites"] = favMedia(anime)
|
||||
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
||||
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
|
||||
sorted["Favourites"]?.forEach { fav ->
|
||||
all.find { it.id == fav.id }?.let {
|
||||
fav.userProgress = it.userProgress
|
||||
}
|
||||
}
|
||||
|
||||
sorted["All"] = all
|
||||
|
||||
val sort = sortOrder ?: options?.rowOrder
|
||||
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
||||
else PrefManager.getVal(PrefName.MangaListSortOrder)
|
||||
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
||||
for (i in sorted.keys) {
|
||||
when (sort) {
|
||||
"score" -> sorted[i]?.sortWith { b, a ->
|
||||
@@ -432,11 +653,19 @@ class AnilistQueries {
|
||||
}
|
||||
|
||||
|
||||
suspend fun getGenresAndTags(activity: Activity): Boolean {
|
||||
var genres: ArrayList<String>? = loadData("genres_list", activity)
|
||||
var tags: Map<Boolean, List<String>>? = loadData("tags_map", activity)
|
||||
suspend fun getGenresAndTags(): Boolean {
|
||||
var genres: ArrayList<String>? = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||
.toMutableList() as ArrayList<String>?
|
||||
val adultTags = PrefManager.getVal<Set<String>>(PrefName.TagsListIsAdult).toMutableList()
|
||||
val nonAdultTags =
|
||||
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
|
||||
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
|
||||
mapOf(
|
||||
true to adultTags,
|
||||
false to nonAdultTags
|
||||
)
|
||||
|
||||
if (genres == null) {
|
||||
if (genres.isNullOrEmpty()) {
|
||||
executeQuery<Query.GenreCollection>(
|
||||
"""{GenreCollection}""",
|
||||
force = true,
|
||||
@@ -446,7 +675,7 @@ class AnilistQueries {
|
||||
forEach {
|
||||
genres?.add(it)
|
||||
}
|
||||
saveData("genres_list", genres!!)
|
||||
PrefManager.setVal(PrefName.GenresList, genres?.toSet())
|
||||
}
|
||||
}
|
||||
if (tags == null) {
|
||||
@@ -464,10 +693,11 @@ class AnilistQueries {
|
||||
true to adult,
|
||||
false to good
|
||||
)
|
||||
saveData("tags_map", tags)
|
||||
PrefManager.setVal(PrefName.TagsListIsAdult, adult.toSet())
|
||||
PrefManager.setVal(PrefName.TagsListNonAdult, good.toSet())
|
||||
}
|
||||
}
|
||||
return if (genres != null && tags != null) {
|
||||
return if (!genres.isNullOrEmpty() && tags != null) {
|
||||
Anilist.genres = genres
|
||||
Anilist.tags = tags
|
||||
true
|
||||
@@ -484,8 +714,37 @@ class AnilistQueries {
|
||||
}
|
||||
}
|
||||
|
||||
private fun <K, V : Serializable> saveSerializableMap(prefKey: String, map: Map<K, V>) {
|
||||
val byteStream = ByteArrayOutputStream()
|
||||
|
||||
ObjectOutputStream(byteStream).use { outputStream ->
|
||||
outputStream.writeObject(map)
|
||||
}
|
||||
val serializedMap = Base64.encodeToString(byteStream.toByteArray(), Base64.DEFAULT)
|
||||
PrefManager.setCustomVal(prefKey, serializedMap)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <K, V : Serializable> loadSerializableMap(prefKey: String): Map<K, V>? {
|
||||
try {
|
||||
val serializedMap = PrefManager.getCustomVal(prefKey, "")
|
||||
if (serializedMap.isEmpty()) return null
|
||||
|
||||
val bytes = Base64.decode(serializedMap, Base64.DEFAULT)
|
||||
val byteArrayStream = ByteArrayInputStream(bytes)
|
||||
|
||||
return ObjectInputStream(byteArrayStream).use { inputStream ->
|
||||
inputStream.readObject() as? Map<K, V>
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getGenreThumbnail(genre: String): Genre? {
|
||||
val genres = loadData<MutableMap<String, Genre>>("genre_thumb") ?: mutableMapOf()
|
||||
val genres: MutableMap<String, Genre> =
|
||||
loadSerializableMap<String, Genre>("genre_thumb")?.toMutableMap()
|
||||
?: mutableMapOf()
|
||||
if (genres.checkGenreTime(genre)) {
|
||||
try {
|
||||
val genreQuery =
|
||||
@@ -498,7 +757,7 @@ class AnilistQueries {
|
||||
it.bannerImage!!,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
saveData("genre_thumb", genres)
|
||||
saveSerializableMap("genre_thumb", genres)
|
||||
return genres[genre]
|
||||
}
|
||||
}
|
||||
@@ -653,7 +912,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
page = pageInfo.currentPage.toString().toIntOrNull() ?: 0,
|
||||
hasNextPage = pageInfo.hasNextPage == true,
|
||||
)
|
||||
} else snackString(currContext()?.getString(R.string.empty_response))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -711,7 +970,7 @@ Page(page:$page,perPage:50) {
|
||||
if (smaller) {
|
||||
val response = execute()?.airingSchedules ?: return null
|
||||
val idArr = mutableListOf<Int>()
|
||||
val listOnly = loadData("recently_list_only") ?: false
|
||||
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||
return response.mapNotNull { i ->
|
||||
i.media?.let {
|
||||
if (!idArr.contains(it.id))
|
||||
|
||||
@@ -5,12 +5,14 @@ import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -19,12 +21,8 @@ import kotlinx.coroutines.launch
|
||||
|
||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val token = sharedPref.getString("discord_token", null)
|
||||
val userid = sharedPref.getString("discord_id", null)
|
||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||
val userid = PrefManager.getVal(PrefName.DiscordId, null as String?)
|
||||
if (userid == null && token != null) {
|
||||
/*if (!Discord.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_discord_user_data))*/
|
||||
@@ -99,12 +97,25 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||
|
||||
suspend fun initHomePage() {
|
||||
val res = Anilist.query.initHomePage()
|
||||
res["currentAnime"]?.let { animeContinue.postValue(it) }
|
||||
res["favoriteAnime"]?.let { animeFav.postValue(it) }
|
||||
res["plannedAnime"]?.let { animePlanned.postValue(it) }
|
||||
res["currentManga"]?.let { mangaContinue.postValue(it) }
|
||||
res["favoriteManga"]?.let { mangaFav.postValue(it) }
|
||||
res["plannedManga"]?.let { mangaPlanned.postValue(it) }
|
||||
res["recommendations"]?.let { recommendation.postValue(it) }
|
||||
}
|
||||
|
||||
suspend fun loadMain(context: FragmentActivity) {
|
||||
Anilist.getSavedToken(context)
|
||||
Anilist.getSavedToken()
|
||||
MAL.getSavedToken(context)
|
||||
Discord.getSavedToken(context)
|
||||
if (loadData<Boolean>("check_update") != false) AppUpdater.check(context)
|
||||
genres.postValue(Anilist.query.getGenresAndTags(context))
|
||||
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||
}
|
||||
genres.postValue(Anilist.query.getGenresAndTags())
|
||||
}
|
||||
|
||||
val empty = MutableLiveData<Boolean>(null)
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
val data: Uri? = intent?.data
|
||||
logger(data.toString())
|
||||
try {
|
||||
Anilist.token =
|
||||
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||
val filename = "anilistToken"
|
||||
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
|
||||
it.write(Anilist.token!!.toByteArray())
|
||||
}
|
||||
PrefManager.setVal(PrefName.AnilistToken, Anilist.token ?: "")
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import ani.dantotsu.loadMedia
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class UrlMedia : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||
var isMAL = false
|
||||
|
||||
@@ -105,6 +105,40 @@ class Query {
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CombinedMediaListResponse(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("current") val current: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("planned") val planned: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("repeating") val repeating: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class HomePageMedia(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("currentAnime") val currentAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("repeatingAnime") val repeatingAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("plannedAnime") val plannedAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("currentManga") val currentManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("repeatingManga") val repeatingManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("plannedManga") val plannedManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("recommendationQuery") val recommendationQuery: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("recommendationPlannedQueryAnime") val recommendationPlannedQueryAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("recommendationPlannedQueryManga") val recommendationPlannedQueryManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GenreCollection(
|
||||
@SerialName("data")
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
import android.content.Context
|
||||
|
||||
interface CrashlyticsInterface {
|
||||
fun initialize(context: Context)
|
||||
fun logException(e: Throwable)
|
||||
fun log(message: String)
|
||||
fun setUserId(id: String)
|
||||
fun setCustomKey(key: String, value: String)
|
||||
fun setCrashlyticsCollectionEnabled(enabled: Boolean)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
import android.content.Context
|
||||
|
||||
class CrashlyticsStub : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
//no-op
|
||||
}
|
||||
override fun logException(e: Throwable) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun log(message: String) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun setUserId(id: String) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun setCustomKey(key: String, value: String) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun setCrashlyticsCollectionEnabled(enabled: Boolean) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,9 +3,10 @@ package ani.dantotsu.connections.discord
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.edit
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWith
|
||||
import io.noties.markwon.Markwon
|
||||
@@ -18,37 +19,20 @@ object Discord {
|
||||
var userid: String? = null
|
||||
var avatar: String? = null
|
||||
|
||||
const val TOKEN = "discord_token"
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
token = PrefManager.getVal(
|
||||
PrefName.DiscordToken, null as String?
|
||||
)
|
||||
token = sharedPref.getString(TOKEN, null)
|
||||
return token != null
|
||||
}
|
||||
|
||||
fun saveToken(context: Context, token: String) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPref.edit {
|
||||
putString(TOKEN, token)
|
||||
commit()
|
||||
}
|
||||
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPref.edit {
|
||||
remove(TOKEN)
|
||||
commit()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.DiscordToken)
|
||||
|
||||
tryWith(true) {
|
||||
val dir = File(context.filesDir?.parentFile, "app_webview")
|
||||
|
||||
@@ -24,6 +24,8 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.serializers.Presence
|
||||
import ani.dantotsu.connections.discord.serializers.User
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
@@ -149,19 +151,11 @@ class DiscordService : Service() {
|
||||
}
|
||||
|
||||
fun saveProfile(response: String) {
|
||||
val sharedPref = baseContext.getSharedPreferences(
|
||||
baseContext.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val user = json.decodeFromString<User.Response>(response).d.user
|
||||
log("User data: $user")
|
||||
with(sharedPref.edit()) {
|
||||
putString("discord_username", user.username)
|
||||
putString("discord_id", user.id)
|
||||
putString("discord_avatar", user.avatar)
|
||||
apply()
|
||||
}
|
||||
|
||||
PrefManager.setVal(PrefName.DiscordUserName, user.username)
|
||||
PrefManager.setVal(PrefName.DiscordId, user.id)
|
||||
PrefManager.setVal(PrefName.DiscordAvatar, user.avatar)
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
@@ -318,17 +312,13 @@ class DiscordService : Service() {
|
||||
}
|
||||
|
||||
fun getToken(context: Context): String {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val token = sharedPref.getString(Discord.TOKEN, null)
|
||||
if (token == null) {
|
||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||
return if (token == null) {
|
||||
log("WebSocket: Token not found")
|
||||
errorNotification("Could not set the presence", "token not found")
|
||||
return ""
|
||||
""
|
||||
} else {
|
||||
return token
|
||||
token
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
@@ -20,7 +19,7 @@ class Login : AppCompatActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = getProcessName()
|
||||
|
||||
@@ -8,9 +8,9 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.mal.MAL.clientId
|
||||
import ani.dantotsu.connections.mal.MAL.saveResponse
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
@@ -21,12 +21,12 @@ import kotlinx.coroutines.launch
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
try {
|
||||
val data: Uri = intent?.data
|
||||
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
||||
val codeChallenge: String = loadData("malCodeChallenge", this)
|
||||
val codeChallenge = PrefManager.getVal(PrefName.MALCodeChallenge, null as String?)
|
||||
?: throw Exception(getString(R.string.mal_login_code_challenge_not_found))
|
||||
val code = data.getQueryParameter("code")
|
||||
?: throw Exception(getString(R.string.mal_login_code_not_present))
|
||||
|
||||
@@ -9,13 +9,12 @@ import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
object MAL {
|
||||
@@ -34,7 +33,7 @@ object MAL {
|
||||
.replace("/", "_")
|
||||
.replace("\n", "")
|
||||
|
||||
saveData("malCodeChallenge", codeChallenge, context)
|
||||
PrefManager.setVal(PrefName.MALCodeChallenge, codeChallenge)
|
||||
val request =
|
||||
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
|
||||
try {
|
||||
@@ -47,11 +46,9 @@ object MAL {
|
||||
}
|
||||
}
|
||||
|
||||
private const val MAL_TOKEN = "malToken"
|
||||
|
||||
private suspend fun refreshToken(): ResponseToken? {
|
||||
return tryWithSuspend {
|
||||
val token = loadData<ResponseToken>(MAL_TOKEN)
|
||||
val token = PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
|
||||
val res = client.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
@@ -69,8 +66,9 @@ object MAL {
|
||||
|
||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
||||
return tryWithSuspend(false) {
|
||||
var res: ResponseToken = loadData(MAL_TOKEN, context)
|
||||
?: return@tryWithSuspend false
|
||||
var res: ResponseToken =
|
||||
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||
?: return@tryWithSuspend false
|
||||
if (System.currentTimeMillis() > res.expiresIn)
|
||||
res = refreshToken()
|
||||
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
|
||||
@@ -84,14 +82,12 @@ object MAL {
|
||||
username = null
|
||||
userid = null
|
||||
avatar = null
|
||||
if (MAL_TOKEN in context.fileList()) {
|
||||
File(context.filesDir, MAL_TOKEN).delete()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.MALToken)
|
||||
}
|
||||
|
||||
fun saveResponse(res: ResponseToken) {
|
||||
res.expiresIn += System.currentTimeMillis()
|
||||
saveData(MAL_TOKEN, res)
|
||||
PrefManager.setVal(PrefName.MALToken, res)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -100,6 +96,10 @@ object MAL {
|
||||
@SerialName("expires_in") var expiresIn: Long,
|
||||
@SerialName("access_token") val accessToken: String,
|
||||
@SerialName("refresh_token") val refreshToken: String,
|
||||
) : java.io.Serializable
|
||||
) : java.io.Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID = 1L
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class DownloadContainerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
setContentView(R.layout.activity_container)
|
||||
|
||||
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
val fragment = Class.forName(fragmentClassName).newInstance() as Fragment
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,56 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
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 mangaDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.MANGA }
|
||||
val animeDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.ANIME }
|
||||
val novelDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.NOVEL }
|
||||
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()
|
||||
PrefManager.setVal(PrefName.DownloadsKeys, jsonString)
|
||||
}
|
||||
|
||||
private fun loadDownloads(): List<Download> {
|
||||
val jsonString = prefs.getString("downloads_key", null)
|
||||
private fun loadDownloads(): List<DownloadedType> {
|
||||
val jsonString = PrefManager.getVal(PrefName.DownloadsKeys, null as String?)
|
||||
return if (jsonString != null) {
|
||||
val type = object : TypeToken<List<Download>>() {}.type
|
||||
val type = object : TypeToken<List<DownloadedType>>() {}.type
|
||||
gson.fromJson(jsonString, type)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addDownload(download: Download) {
|
||||
downloadsList.add(download)
|
||||
fun addDownload(downloadedType: DownloadedType) {
|
||||
downloadsList.add(downloadedType)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(download: Download) {
|
||||
downloadsList.remove(download)
|
||||
removeDirectory(download)
|
||||
fun removeDownload(downloadedType: DownloadedType) {
|
||||
downloadsList.remove(downloadedType)
|
||||
removeDirectory(downloadedType)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: Download.Type) {
|
||||
val subDirectory = if (type == Download.Type.MANGA) {
|
||||
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
@@ -71,21 +70,33 @@ class DownloadsManager(private val context: Context) {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
cleanDownloads()
|
||||
}
|
||||
downloadsList.removeAll { it.title == title }
|
||||
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(Download.Type.MANGA)
|
||||
cleanDownload(Download.Type.ANIME)
|
||||
cleanDownload(Download.Type.NOVEL)
|
||||
cleanDownload(DownloadedType.Type.MANGA)
|
||||
cleanDownload(DownloadedType.Type.ANIME)
|
||||
cleanDownload(DownloadedType.Type.NOVEL)
|
||||
}
|
||||
|
||||
private fun cleanDownload(type: Download.Type) {
|
||||
private fun cleanDownload(type: DownloadedType.Type) {
|
||||
// remove all folders that are not in the downloads list
|
||||
val subDirectory = if (type == Download.Type.MANGA) {
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
@@ -94,18 +105,18 @@ class DownloadsManager(private val context: Context) {
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory"
|
||||
)
|
||||
val downloadsSubList = if (type == Download.Type.MANGA) {
|
||||
mangaDownloads
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
animeDownloads
|
||||
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
||||
mangaDownloadedTypes
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
animeDownloadedTypes
|
||||
} else {
|
||||
novelDownloads
|
||||
novelDownloadedTypes
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val files = directory.listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (!downloadsSubList.any { it.title == file.name }) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
val deleted = file.deleteRecursively()
|
||||
}
|
||||
}
|
||||
@@ -122,11 +133,11 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<Download>) //for debugging
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||
{
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/downloads.json"
|
||||
)
|
||||
if (file.parentFile?.exists() == false) {
|
||||
@@ -138,25 +149,33 @@ class DownloadsManager(private val context: Context) {
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
|
||||
fun queryDownload(download: Download): Boolean {
|
||||
return downloadsList.contains(download)
|
||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||
return downloadsList.contains(downloadedType)
|
||||
}
|
||||
|
||||
private fun removeDirectory(download: Download) {
|
||||
val directory = if (download.type == Download.Type.MANGA) {
|
||||
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/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -173,26 +192,26 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
|
||||
val directory = if (download.type == Download.Type.MANGA) {
|
||||
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/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${download.title}/${download.chapter}"
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
val destination = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/${download.title}/${download.chapter}"
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val copied = directory.copyRecursively(destination, true)
|
||||
@@ -206,10 +225,10 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: Download.Type) {
|
||||
val directory = if (type == Download.Type.MANGA) {
|
||||
fun purgeDownloads(type: DownloadedType.Type) {
|
||||
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
} else if (type == Download.Type.ANIME) {
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
@@ -233,11 +252,56 @@ class DownloadsManager(private val context: Context) {
|
||||
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 Download(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME,
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
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.connections.crashlytics.CrashlyticsInterface
|
||||
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.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
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_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())
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
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")
|
||||
PrefManager.getAnimeDownloadPreferences().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()
|
||||
Injekt.get<CrashlyticsInterface>().logException(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
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
|
||||
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: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
|
||||
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 = PrefManager.getVal(PrefName.OfflineView)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package ani.dantotsu.download.anime
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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.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.content.ContextCompat
|
||||
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.connections.crashlytics.CrashlyticsInterface
|
||||
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.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.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
|
||||
view.rootView.fitsSystemWindows = true
|
||||
}
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||
searchView.addTextChangedListener(object : TextWatcher {
|
||||
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: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
var selected = when (style) {
|
||||
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
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
}
|
||||
|
||||
layoutcompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView1)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
}
|
||||
|
||||
gridView =
|
||||
if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
|
||||
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),
|
||||
null
|
||||
)
|
||||
} ?: 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 =
|
||||
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
|
||||
?: emptySet()
|
||||
if (mediaIds.isEmpty()) {
|
||||
snackString("No media found") // if this happens, terrible things have happened
|
||||
}
|
||||
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)
|
||||
|
||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||
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 < 0
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
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 tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
}
|
||||
downloads = newAnimeDownloads
|
||||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
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
|
||||
})
|
||||
.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())
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
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 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())
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = 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?,
|
||||
)
|
||||
@@ -18,7 +18,8 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
@@ -29,7 +30,6 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
@@ -76,7 +76,7 @@ class MangaDownloaderService : Service() {
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Manga Download Progress")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(0, 0, false)
|
||||
@@ -187,7 +187,8 @@ class MangaDownloaderService : Service() {
|
||||
true
|
||||
}
|
||||
|
||||
val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
//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())
|
||||
@@ -196,15 +197,12 @@ class MangaDownloaderService : Service() {
|
||||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
// Limit the number of simultaneous downloads from the task
|
||||
if (deferredList.size >= task.simultaneousDownloads) {
|
||||
// Wait for all deferred to complete and clear the list
|
||||
deferredList.awaitAll()
|
||||
deferredList.clear()
|
||||
if (deferredMap.size >= task.simultaneousDownloads) {
|
||||
deferredMap.values.awaitAll()
|
||||
deferredMap.clear()
|
||||
}
|
||||
|
||||
// Download the image and add to deferred list
|
||||
val deferred = async(Dispatchers.IO) {
|
||||
deferredMap[index] = async(Dispatchers.IO) {
|
||||
var bitmap: Bitmap? = null
|
||||
var retryCount = 0
|
||||
|
||||
@@ -217,7 +215,6 @@ class MangaDownloaderService : Service() {
|
||||
retryCount++
|
||||
}
|
||||
|
||||
// Cache the image if successful
|
||||
if (bitmap != null) {
|
||||
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
||||
}
|
||||
@@ -233,12 +230,10 @@ class MangaDownloaderService : Service() {
|
||||
|
||||
bitmap
|
||||
}
|
||||
|
||||
deferredList.add(deferred)
|
||||
}
|
||||
|
||||
// Wait for any remaining deferred to complete
|
||||
deferredList.awaitAll()
|
||||
deferredMap.values.awaitAll()
|
||||
|
||||
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||
.setProgress(0, 0, false)
|
||||
@@ -246,10 +241,10 @@ class MangaDownloaderService : Service() {
|
||||
|
||||
saveMediaInfo(task)
|
||||
downloadsManager.addDownload(
|
||||
Download(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
Download.Type.MANGA
|
||||
DownloadedType.Type.MANGA
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.chapter)
|
||||
@@ -258,7 +253,7 @@ class MangaDownloaderService : Service() {
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading file: ${e.message}")
|
||||
snackString("Exception while downloading file: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
broadcastDownloadFailed(task.chapter)
|
||||
}
|
||||
}
|
||||
@@ -288,7 +283,7 @@ class MangaDownloaderService : Service() {
|
||||
} catch (e: Exception) {
|
||||
println("Exception while saving image: ${e.message}")
|
||||
snackString("Exception while saving image: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +309,16 @@ class MangaDownloaderService : Service() {
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
try {
|
||||
file.writeText(jsonString)
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@MangaDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
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
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
|
||||
class OfflineMangaAdapter(
|
||||
@@ -19,6 +23,7 @@ class OfflineMangaAdapter(
|
||||
private val inflater: LayoutInflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var originalItems: List<OfflineMangaModel> = items
|
||||
private var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return items.size
|
||||
@@ -32,23 +37,47 @@ class OfflineMangaAdapter(
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
var view = convertView
|
||||
if (view == null) {
|
||||
view = inflater.inflate(R.layout.item_media_compact, parent, false)
|
||||
|
||||
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 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
|
||||
// For example:
|
||||
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 {
|
||||
@@ -74,4 +103,9 @@ class OfflineMangaAdapter(
|
||||
this.originalItems = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun notifyNewGrid() {
|
||||
style = PrefManager.getVal(PrefName.OfflineView)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
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
|
||||
@@ -13,27 +10,38 @@ import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.LayoutAnimationController
|
||||
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.content.ContextCompat
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
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.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
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
|
||||
@@ -41,23 +49,24 @@ 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
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
|
||||
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
|
||||
@@ -69,18 +78,16 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||
animeUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show(
|
||||
(it.context as AppCompatActivity).supportFragmentManager,
|
||||
"dialog"
|
||||
)
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
|
||||
view.rootView.fitsSystemWindows = true
|
||||
}
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||
searchView.addTextChangedListener(object : TextWatcher {
|
||||
@@ -94,54 +101,108 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
onSearchQuery(s.toString())
|
||||
}
|
||||
})
|
||||
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
var selected = when (style) {
|
||||
0 -> layoutList
|
||||
1 -> layoutcompact
|
||||
else -> layoutList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
fun selected(it: ImageView) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
|
||||
layoutList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
|
||||
}
|
||||
|
||||
layoutcompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView1)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
}
|
||||
gridView =
|
||||
if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
|
||||
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.setOnItemClickListener { parent, view, position, id ->
|
||||
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.mangaDownloads.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloads.firstOrNull { it.title == item.title }
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
media?.let {
|
||||
startActivity(
|
||||
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true)
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
}
|
||||
|
||||
gridView.setOnItemLongClickListener { parent, view, position, id ->
|
||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) {
|
||||
Download.Type.MANGA
|
||||
} else {
|
||||
Download.Type.NOVEL
|
||||
}
|
||||
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)
|
||||
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()
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
true
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onSearchQuery(query: String) {
|
||||
@@ -150,41 +211,35 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
initActivity(requireActivity())
|
||||
|
||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||
var 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)
|
||||
}
|
||||
|
||||
scrollTop.setOnClickListener {
|
||||
//TODO: scroll to top
|
||||
}
|
||||
// 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 < 0
|
||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -211,20 +266,20 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct()
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct()
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val _downloads = downloadManager.novelDownloads.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
}
|
||||
@@ -232,17 +287,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
}
|
||||
|
||||
private fun getMedia(download: Download): Media? {
|
||||
val type = if (download.type == Download.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
DownloadedType.Type.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${download.title}"
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
return try {
|
||||
@@ -257,49 +310,73 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel {
|
||||
val type = if (download.type == Download.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (download.type == Download.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
DownloadedType.Type.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${download.title}"
|
||||
"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(download)!!
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val title = mediaModel.nameMAL ?: mediaModel.nameRomaji
|
||||
} 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 = false
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
|
||||
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", false, false, null)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OfflineMangaSearchListener {
|
||||
fun onSearchQuery(query: String)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ 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 image: Uri?,
|
||||
val banner: Uri?
|
||||
)
|
||||
@@ -17,13 +17,13 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
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
|
||||
@@ -75,7 +75,7 @@ class NovelDownloaderService : Service() {
|
||||
builder =
|
||||
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Novel Download Progress")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(0, 0, false)
|
||||
@@ -330,10 +330,10 @@ class NovelDownloaderService : Service() {
|
||||
|
||||
saveMediaInfo(task)
|
||||
downloadsManager.addDownload(
|
||||
Download(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
Download.Type.NOVEL
|
||||
DownloadedType.Type.NOVEL
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
@@ -342,7 +342,7 @@ class NovelDownloaderService : Service() {
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading .epub: ${e.message}")
|
||||
snackString("Exception while downloading .epub: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
broadcastDownloadFailed(task.originalLink)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
|
||||
import ani.dantotsu.R
|
||||
|
||||
@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 {
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||
@@ -1,12 +1,21 @@
|
||||
package ani.dantotsu.download.video
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
@@ -15,19 +24,25 @@ import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.Requirements
|
||||
import androidx.media3.ui.TrackSelectionDialogBuilder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -37,6 +52,8 @@ import java.util.concurrent.*
|
||||
|
||||
object Helper {
|
||||
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
@@ -82,27 +99,13 @@ object Helper {
|
||||
)
|
||||
downloadHelper.prepare(object : DownloadHelper.Callback {
|
||||
override fun onPrepared(helper: DownloadHelper) {
|
||||
TrackSelectionDialogBuilder(
|
||||
context, "Select thingy", helper.getTracks(0).groups
|
||||
) { _, overrides ->
|
||||
val params = TrackSelectionParameters.Builder(context)
|
||||
overrides.forEach {
|
||||
params.addOverride(it.value)
|
||||
}
|
||||
helper.addTrackSelection(0, params.build())
|
||||
MyDownloadService
|
||||
helper.getDownloadRequest(null).let {
|
||||
DownloadService.sendAddDownload(
|
||||
context,
|
||||
MyDownloadService::class.java,
|
||||
helper.getDownloadRequest(null),
|
||||
ExoplayerDownloadService::class.java,
|
||||
it,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,13 +117,13 @@ object Helper {
|
||||
|
||||
|
||||
private var download: DownloadManager? = null
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads"
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
|
||||
@Synchronized
|
||||
@UnstableApi
|
||||
fun downloadManager(context: Context): DownloadManager {
|
||||
return download ?: let {
|
||||
val database = StandaloneDatabaseProvider(context)
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
@@ -133,17 +136,42 @@ object Helper {
|
||||
}
|
||||
dataSource
|
||||
}
|
||||
DownloadManager(
|
||||
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||
val downloadManager = DownloadManager(
|
||||
context,
|
||||
database,
|
||||
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database),
|
||||
getSimpleCache(context),
|
||||
dataSourceFactory,
|
||||
Executor(Runnable::run)
|
||||
executorService
|
||||
).apply {
|
||||
requirements =
|
||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,4 +187,102 @@ object Helper {
|
||||
}
|
||||
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,
|
||||
PrefManager.getAnimeDownloadPreferences().getString(
|
||||
animeDownloadTask.getTaskName(),
|
||||
""
|
||||
) ?: "",
|
||||
false
|
||||
)
|
||||
PrefManager.getAnimeDownloadPreferences().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
|
||||
}
|
||||
}
|
||||
@@ -27,13 +27,13 @@ import ani.dantotsu.connections.anilist.AnilistAnimeViewModel
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.databinding.FragmentAnimeBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.ProgressAdapter
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -47,9 +47,7 @@ import kotlin.math.min
|
||||
class AnimeFragment : Fragment() {
|
||||
private var _binding: FragmentAnimeBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private lateinit var animePageAdapter: AnimePageAdapter
|
||||
|
||||
val model: AnilistAnimeViewModel by activityViewModels()
|
||||
|
||||
@@ -95,7 +93,7 @@ class AnimeFragment : Fragment() {
|
||||
|
||||
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||
|
||||
val animePageAdapter = AnimePageAdapter()
|
||||
animePageAdapter = AnimePageAdapter()
|
||||
|
||||
var loading = true
|
||||
if (model.notSet) {
|
||||
@@ -215,7 +213,7 @@ class AnimeFragment : Fragment() {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateTrending(
|
||||
MediaAdaptor(
|
||||
if (uiSettings.smallView) 3 else 2,
|
||||
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
|
||||
it,
|
||||
requireActivity(),
|
||||
viewPager = animePageAdapter.trendingViewPager
|
||||
@@ -266,7 +264,11 @@ class AnimeFragment : Fragment() {
|
||||
model.loaded = true
|
||||
model.loadTrending(1)
|
||||
model.loadUpdated()
|
||||
model.loadPopular("ANIME", sort = Anilist.sortBy[1])
|
||||
model.loadPopular(
|
||||
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularAnimeList
|
||||
)
|
||||
)
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.animeRefresh?.isRefreshing = false
|
||||
@@ -277,6 +279,11 @@ class AnimeFragment : Fragment() {
|
||||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||
if (animePageAdapter.trendingViewPager != null) {
|
||||
binding.root.requestApplyInsets()
|
||||
binding.root.requestLayout()
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -22,7 +21,6 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.CalendarActivity
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
@@ -33,7 +31,8 @@ import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -44,8 +43,6 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
|
||||
val binding =
|
||||
@@ -68,17 +65,12 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
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())
|
||||
}
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = (-108f).px
|
||||
}
|
||||
|
||||
@@ -98,10 +90,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
binding.animeUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.ANIME).show(
|
||||
(it.context as AppCompatActivity).supportFragmentManager,
|
||||
"dialog"
|
||||
)
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
listOf(
|
||||
@@ -133,8 +124,13 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
binding.animeIncludeList.visibility =
|
||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||
|
||||
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
|
||||
|
||||
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||
onIncludeListClick.invoke(isChecked)
|
||||
|
||||
PrefManager.setVal(PrefName.PopularAnimeList, isChecked)
|
||||
}
|
||||
if (ready.value == false)
|
||||
ready.postValue(true)
|
||||
@@ -173,12 +169,12 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
)
|
||||
|
||||
binding.animeTrendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animeTitleContainer.startAnimation(setSlideUp())
|
||||
binding.animeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animeSeasonsCont.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateRecent(adaptor: MediaAdaptor) {
|
||||
@@ -193,11 +189,11 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeRecently.visibility = View.VISIBLE
|
||||
binding.animeRecently.startAnimation(setSlideUp(uiSettings))
|
||||
binding.animeRecently.startAnimation(setSlideUp())
|
||||
binding.animeUpdatedRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animePopular.visibility = View.VISIBLE
|
||||
binding.animePopular.startAnimation(setSlideUp(uiSettings))
|
||||
binding.animePopular.startAnimation(setSlideUp())
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
|
||||
@@ -27,7 +27,6 @@ import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.FragmentHomeBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
@@ -37,7 +36,8 @@ import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -70,14 +70,13 @@ class HomeFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val scope = lifecycleScope
|
||||
var uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
fun load() {
|
||||
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
|
||||
binding.homeUserName.text = Anilist.username
|
||||
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
||||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
||||
if (!uiSettings.bannerAnimations) binding.homeUserBg.pause()
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
|
||||
binding.homeUserBg.loadImage(Anilist.bg)
|
||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||
|
||||
@@ -98,23 +97,24 @@ class HomeFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||
binding.homeUserDataContainer.visibility = View.VISIBLE
|
||||
binding.homeUserDataContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideUp(), 0.25f)
|
||||
binding.homeAnimeList.visibility = View.VISIBLE
|
||||
binding.homeMangaList.visibility = View.VISIBLE
|
||||
binding.homeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
else {
|
||||
snackString(currContext()?.getString(R.string.please_reload))
|
||||
}
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show(
|
||||
parentFragmentManager,
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
|
||||
dialogFragment.show(
|
||||
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
|
||||
"dialog"
|
||||
)
|
||||
}
|
||||
@@ -126,20 +126,18 @@ class HomeFragment : Fragment() {
|
||||
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
var reached = false
|
||||
val duration = (uiSettings.animationSpeed * 200).toLong()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||
reached = true
|
||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
|
||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||
reached = true
|
||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||
.start()
|
||||
} else {
|
||||
if (reached) {
|
||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||
.start()
|
||||
} else {
|
||||
if (reached) {
|
||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,13 +205,13 @@ class HomeFragment : Fragment() {
|
||||
)
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
|
||||
} else {
|
||||
empty.visibility = View.VISIBLE
|
||||
}
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp(uiSettings))
|
||||
title.startAnimation(setSlideUp())
|
||||
progress.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -296,25 +294,25 @@ class HomeFragment : Fragment() {
|
||||
binding.homeRecommended
|
||||
)
|
||||
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||
|
||||
model.empty.observe(viewLifecycleOwner) {
|
||||
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
|
||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||
binding.homeDantotsuContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeDantotsuContainer.startAnimation(setSlideUp())
|
||||
binding.homeDantotsuIcon.setSafeOnClickListener {
|
||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||
}
|
||||
}
|
||||
|
||||
val array = arrayOf(
|
||||
Runnable { runBlocking { model.setAnimeContinue() } },
|
||||
Runnable { runBlocking { model.setAnimeFav() } },
|
||||
Runnable { runBlocking { model.setAnimePlanned() } },
|
||||
Runnable { runBlocking { model.setMangaContinue() } },
|
||||
Runnable { runBlocking { model.setMangaFav() } },
|
||||
Runnable { runBlocking { model.setMangaPlanned() } },
|
||||
Runnable { runBlocking { model.setRecommendation() } }
|
||||
"AnimeContinue",
|
||||
"AnimeFav",
|
||||
"AnimePlanned",
|
||||
"MangaContinue",
|
||||
"MangaFav",
|
||||
"MangaPlanned",
|
||||
"Recommendation"
|
||||
)
|
||||
|
||||
val containers = arrayOf(
|
||||
@@ -331,8 +329,6 @@ class HomeFragment : Fragment() {
|
||||
live.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
withContext(Dispatchers.IO) {
|
||||
//Get userData First
|
||||
getUserId(requireContext()) {
|
||||
@@ -341,9 +337,13 @@ class HomeFragment : Fragment() {
|
||||
model.loaded = true
|
||||
model.setListImages()
|
||||
var empty = true
|
||||
val homeLayoutShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayoutShow)
|
||||
runBlocking {
|
||||
model.initHomePage()
|
||||
}
|
||||
(array.indices).forEach { i ->
|
||||
if (uiSettings.homeLayoutShow[i]) {
|
||||
array[i].run()
|
||||
if (homeLayoutShow.elementAt(i)) {
|
||||
empty = false
|
||||
} else withContext(Dispatchers.Main) {
|
||||
containers[i].visibility = View.GONE
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.FragmentLoginBinding
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||
import ani.dantotsu.toast
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
||||
class LoginFragment : Fragment() {
|
||||
|
||||
@@ -28,5 +37,100 @@ class LoginFragment : Fragment() {
|
||||
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
|
||||
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
|
||||
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
|
||||
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
|
||||
|
||||
val openDocumentLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
val jsonString =
|
||||
requireActivity().contentResolver.openInputStream(uri)?.readBytes()
|
||||
?: throw Exception("Error reading file")
|
||||
val name =
|
||||
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
|
||||
//.sani is encrypted, .ani is not
|
||||
if (name.endsWith(".sani")) {
|
||||
passwordAlertDialog() { password ->
|
||||
if (password != null) {
|
||||
val salt = jsonString.copyOfRange(0, 16)
|
||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||
val decryptedJson = try {
|
||||
PreferenceKeystore.decryptWithPassword(
|
||||
password,
|
||||
encrypted,
|
||||
salt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast("Incorrect password")
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
if (PreferencePackager.unpack(decryptedJson))
|
||||
restartApp()
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
} else if (name.endsWith(".ani")) {
|
||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
||||
if (PreferencePackager.unpack(decryptedJson))
|
||||
restartApp()
|
||||
} else {
|
||||
toast("Invalid file type")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast("Error importing settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.importSettingsButton.setOnClickListener {
|
||||
openDocumentLauncher.launch(arrayOf("*/*"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView =
|
||||
LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null)
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
subtitleTextView?.text = "Enter your password to decrypt the file"
|
||||
|
||||
val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
password.fill('0')
|
||||
dialog.dismiss()
|
||||
callback(null)
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
|
||||
// Override the positive button here
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
|
||||
if (editText?.text?.isNotBlank() == true) {
|
||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
||||
dialog.dismiss()
|
||||
callback(password)
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restartApp() {
|
||||
val intent = Intent(requireActivity(), requireActivity().javaClass)
|
||||
requireActivity().finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,12 +25,12 @@ import ani.dantotsu.connections.anilist.AnilistMangaViewModel
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.ProgressAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -43,9 +43,7 @@ import kotlin.math.min
|
||||
class MangaFragment : Fragment() {
|
||||
private var _binding: FragmentMangaBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private lateinit var mangaPageAdapter: MangaPageAdapter
|
||||
|
||||
val model: AnilistMangaViewModel by activityViewModels()
|
||||
|
||||
@@ -90,7 +88,7 @@ class MangaFragment : Fragment() {
|
||||
|
||||
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||
|
||||
val mangaPageAdapter = MangaPageAdapter()
|
||||
mangaPageAdapter = MangaPageAdapter()
|
||||
var loading = true
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
@@ -173,7 +171,7 @@ class MangaFragment : Fragment() {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrending(
|
||||
MediaAdaptor(
|
||||
if (uiSettings.smallView) 3 else 2,
|
||||
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
|
||||
it,
|
||||
requireActivity(),
|
||||
viewPager = mangaPageAdapter.trendingViewPager
|
||||
@@ -240,7 +238,11 @@ class MangaFragment : Fragment() {
|
||||
model.loaded = true
|
||||
model.loadTrending()
|
||||
model.loadTrendingNovel()
|
||||
model.loadPopular("MANGA", sort = Anilist.sortBy[1])
|
||||
model.loadPopular(
|
||||
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularMangaList
|
||||
)
|
||||
)
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.mangaRefresh?.isRefreshing = false
|
||||
@@ -251,6 +253,11 @@ class MangaFragment : Fragment() {
|
||||
|
||||
override fun onResume() {
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -22,7 +21,6 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
@@ -32,7 +30,8 @@ import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -43,8 +42,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
|
||||
val binding =
|
||||
@@ -67,17 +64,12 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
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())
|
||||
}
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
|
||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = (-108f).px
|
||||
}
|
||||
|
||||
@@ -93,10 +85,9 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
|
||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.MANGA).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 {
|
||||
@@ -126,10 +117,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
|
||||
binding.mangaIncludeList.visibility =
|
||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||
|
||||
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
|
||||
|
||||
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||
onIncludeListClick.invoke(isChecked)
|
||||
}
|
||||
|
||||
PrefManager.setVal(PrefName.PopularMangaList, isChecked)
|
||||
}
|
||||
if (ready.value == false)
|
||||
ready.postValue(true)
|
||||
}
|
||||
@@ -164,10 +159,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
|
||||
binding.mangaTrendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.mangaTitleContainer.startAnimation(setSlideUp())
|
||||
binding.mangaListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateNovel(adaptor: MediaAdaptor) {
|
||||
@@ -182,11 +177,11 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.mangaNovel.visibility = View.VISIBLE
|
||||
binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
|
||||
binding.mangaNovel.startAnimation(setSlideUp())
|
||||
binding.mangaNovelRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.mangaPopular.visibility = View.VISIBLE
|
||||
binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
|
||||
binding.mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
@@ -17,49 +19,57 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
||||
import ani.dantotsu.download.anime.OfflineAnimeFragment
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.offline.OfflineFragment
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.selectedOption
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
|
||||
class NoInternet : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityNoInternetBinding
|
||||
lateinit var bottomBar: AnimatedBottomBar
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val backgroundDrawable = bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
bottomBar.background = backgroundDrawable
|
||||
}
|
||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
|
||||
var doubleBackToExitPressedOnce = false
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
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
|
||||
selectedOption = PrefManager.getVal(PrefName.DefaultStartUpTab)
|
||||
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
@@ -70,7 +80,7 @@ class NoInternet : AppCompatActivity() {
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
@@ -96,12 +106,11 @@ class NoInternet : AppCompatActivity() {
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
when (position) {
|
||||
0 -> return OfflineFragment()
|
||||
1 -> return OfflineFragment()
|
||||
2 -> return OfflineMangaFragment()
|
||||
return when (position) {
|
||||
0 -> OfflineAnimeFragment()
|
||||
2 -> OfflineMangaFragment()
|
||||
else -> OfflineFragment()
|
||||
}
|
||||
return LoginFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import ani.dantotsu.Refresh
|
||||
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.px
|
||||
import ani.dantotsu.statusBarHeight
|
||||
@@ -36,7 +35,7 @@ class AuthorActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -4,20 +4,23 @@ import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
@@ -34,7 +37,7 @@ class CalendarActivity : AppCompatActivity() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
|
||||
@@ -63,8 +66,7 @@ class CalendarActivity : AppCompatActivity() {
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) {
|
||||
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
|
||||
this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||
binding.root.fitsSystemWindows = true
|
||||
@@ -76,6 +78,10 @@ class CalendarActivity : AppCompatActivity() {
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -11,10 +11,8 @@ import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.databinding.ItemCharacterBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import java.io.Serializable
|
||||
|
||||
class CharacterAdapter(
|
||||
@@ -26,13 +24,10 @@ class CharacterAdapter(
|
||||
return CharacterViewHolder(binding)
|
||||
}
|
||||
|
||||
private val uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root, uiSettings)
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val character = characterList[position]
|
||||
binding.itemCompactRelation.text = character.role + " "
|
||||
binding.itemCompactImage.loadImage(character.image)
|
||||
|
||||
@@ -17,14 +17,13 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
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.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
@@ -38,22 +37,21 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
private lateinit var character: Character
|
||||
private var loaded = false
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
initActivity(this)
|
||||
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.status)
|
||||
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
|
||||
|
||||
banner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
@@ -136,16 +134,16 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
|
||||
binding.characterCover.visibility =
|
||||
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
|
||||
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
||||
if (percentage >= percent && !isCollapsed) {
|
||||
isCollapsed = true
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
if (immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg)
|
||||
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
if (immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.status)
|
||||
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,7 +27,7 @@ class GenreActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityGenreBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -54,7 +54,9 @@ class GenreActivity : AppCompatActivity() {
|
||||
GridLayoutManager(this, (screenWidth / 156f).toInt())
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {
|
||||
model.loadGenres(
|
||||
Anilist.genres ?: loadLocalGenres() ?: arrayListOf()
|
||||
) {
|
||||
MainScope().launch {
|
||||
adapter.addGenre(it)
|
||||
}
|
||||
@@ -62,4 +64,14 @@ class GenreActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLocalGenres(): ArrayList<String>? {
|
||||
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||
.toMutableList() as ArrayList<String>?
|
||||
return if (genres.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
genres
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,19 @@ data class Media(
|
||||
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 {
|
||||
var media: Media? = null
|
||||
var bitmap: Bitmap? = null
|
||||
|
||||
@@ -13,7 +13,10 @@ import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -23,7 +26,8 @@ import ani.dantotsu.databinding.ItemMediaCompactBinding
|
||||
import ani.dantotsu.databinding.ItemMediaLargeBinding
|
||||
import ani.dantotsu.databinding.ItemMediaPageBinding
|
||||
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
@@ -41,9 +45,6 @@ class MediaAdaptor(
|
||||
private val viewPager: ViewPager2? = null,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (type) {
|
||||
0 -> MediaViewHolder(
|
||||
@@ -88,7 +89,7 @@ class MediaAdaptor(
|
||||
when (type) {
|
||||
0 -> {
|
||||
val b = (holder as MediaViewHolder).binding
|
||||
setAnimation(activity, b.root, uiSettings)
|
||||
setAnimation(activity, b.root)
|
||||
val media = mediaList?.getOrNull(position)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
@@ -132,11 +133,11 @@ class MediaAdaptor(
|
||||
|
||||
1 -> {
|
||||
val b = (holder as MediaLargeViewHolder).binding
|
||||
setAnimation(activity, b.root, uiSettings)
|
||||
setAnimation(activity, b.root)
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
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.itemCompactTitle.text = media.userPreferredName
|
||||
@@ -175,16 +176,17 @@ class MediaAdaptor(
|
||||
val b = (holder as MediaPageViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
if (uiSettings.bannerAnimations)
|
||||
if (bannerAnimations)
|
||||
b.itemCompactBanner.setTransitionGenerator(
|
||||
RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed)) as Float)).toLong(),
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
@@ -231,16 +233,17 @@ class MediaAdaptor(
|
||||
val b = (holder as MediaPageSmallViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
if (uiSettings.bannerAnimations)
|
||||
if (bannerAnimations)
|
||||
b.itemCompactBanner.setTransitionGenerator(
|
||||
RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
@@ -300,17 +303,16 @@ class MediaAdaptor(
|
||||
return type
|
||||
}
|
||||
|
||||
fun randomOptionClick() { //used for user list
|
||||
val media = mediaList?.random()
|
||||
if (media != null) {
|
||||
mediaList?.let {
|
||||
clicked(
|
||||
it.indexOf(media),
|
||||
null
|
||||
)
|
||||
}
|
||||
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) :
|
||||
@@ -320,6 +322,7 @@ class MediaAdaptor(
|
||||
itemView.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
@@ -333,6 +336,7 @@ class MediaAdaptor(
|
||||
itemView.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
@@ -347,6 +351,7 @@ class MediaAdaptor(
|
||||
binding.itemCompactImage.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
@@ -362,12 +367,14 @@ class MediaAdaptor(
|
||||
binding.itemCompactImage.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
binding.itemCompactTitleContainer.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
@@ -376,7 +383,7 @@ class MediaAdaptor(
|
||||
}
|
||||
}
|
||||
|
||||
fun clicked(position: Int, bitmap: Bitmap? = null) {
|
||||
fun clicked(position: Int, itemCompactImage: ImageView?, bitmap: Bitmap? = null) {
|
||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||
val media = mediaList?.get(position)
|
||||
if (bitmap != null) MediaSingleton.bitmap = bitmap
|
||||
@@ -385,11 +392,21 @@ class MediaAdaptor(
|
||||
Intent(activity, MediaDetailsActivity::class.java).putExtra(
|
||||
"media",
|
||||
media as Serializable
|
||||
), null
|
||||
),
|
||||
if (itemCompactImage != null) {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
activity,
|
||||
itemCompactImage,
|
||||
ViewCompat.getTransitionName(itemCompactImage)!!
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun longClicked(position: Int): Boolean {
|
||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||
val media = mediaList?.get(position) ?: return false
|
||||
|
||||
@@ -34,7 +34,6 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.media.manga.MangaReadFragment
|
||||
@@ -42,10 +41,9 @@ import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
@@ -65,18 +63,23 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
private val scope = lifecycleScope
|
||||
private val model: MediaDetailsViewModel by viewModels()
|
||||
private lateinit var tabLayout: NavigationBarView
|
||||
private lateinit var uiSettings: UserInterfaceSettings
|
||||
var selected = 0
|
||||
var anime = true
|
||||
private var adult = false
|
||||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
LangSet.setLocale(this)
|
||||
var media: Media = intent.getSerialized("media") ?: return
|
||||
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
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
@@ -84,13 +87,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
//Ui init
|
||||
|
||||
initActivity(this)
|
||||
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.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
||||
|
||||
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -106,20 +107,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
if (uiSettings.bannerAnimations) {
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
if (bannerAnimations) {
|
||||
val adi = AccelerateDecelerateInterpolator()
|
||||
val generator = RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
|
||||
adi
|
||||
)
|
||||
binding.mediaBanner.setTransitionGenerator(generator)
|
||||
}
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||
val viewPager = binding.mediaViewPager
|
||||
tabLayout = binding.mediaTab as NavigationBarView
|
||||
viewPager.isUserInputEnabled = false
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
|
||||
|
||||
val isDownload = intent.getBooleanExtra("download", false)
|
||||
@@ -136,7 +138,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
banner.loadImage(media.banner ?: media.cover, 400)
|
||||
val gestureDetector = GestureDetector(this, object : GesturesListener() {
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
if (!uiSettings.bannerAnimations)
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
|
||||
snackString(getString(R.string.enable_banner_animations))
|
||||
else {
|
||||
binding.mediaBanner.restart()
|
||||
@@ -154,7 +156,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
})
|
||||
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
||||
binding.mediaTitle.text = media.userPreferredName
|
||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||
binding.mediaTitle.text = " ${media.userPreferredName}"
|
||||
binding.incognito.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.mediaTitle.text = media.userPreferredName
|
||||
}
|
||||
binding.mediaTitle.setOnLongClickListener {
|
||||
copyToClipboard(media.userPreferredName)
|
||||
true
|
||||
@@ -273,7 +280,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
} else snackString(getString(R.string.please_login_anilist))
|
||||
}
|
||||
binding.mediaAddToList.setOnLongClickListener {
|
||||
saveData("${media.id}_progressDialog", true)
|
||||
PrefManager.setCustomVal(
|
||||
"${media.id}_progressDialog",
|
||||
true,
|
||||
)
|
||||
snackString(getString(R.string.auto_update_reset))
|
||||
true
|
||||
}
|
||||
@@ -305,7 +315,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
|
||||
adult = media.isAdult
|
||||
|
||||
tabLayout.menu.clear()
|
||||
if (media.anime != null) {
|
||||
viewPager.adapter =
|
||||
@@ -317,7 +326,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
lifecycle,
|
||||
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
|
||||
)
|
||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||
if (media.format == "NOVEL") {
|
||||
tabLayout.inflateMenu(R.menu.novel_menu_detail)
|
||||
} else {
|
||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||
}
|
||||
anime = false
|
||||
}
|
||||
|
||||
@@ -331,7 +344,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
val sel = model.loadSelected(media, isDownload)
|
||||
sel.window = selected
|
||||
model.saveSelected(media.id, sel, this)
|
||||
model.saveSelected(media.id, sel)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -340,7 +353,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
|
||||
if (model.continueMedia == null && media.cameFromContinue) {
|
||||
model.continueMedia = loadData("continue_media") ?: true
|
||||
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
||||
selected = 1
|
||||
}
|
||||
|
||||
@@ -381,7 +394,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
tabLayout.selectedItemId = idFromSelect()
|
||||
if (this::tabLayout.isInitialized) {
|
||||
tabLayout.selectedItemId = idFromSelect()
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@@ -423,7 +438,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
binding.mediaCover.visibility =
|
||||
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
val duration = (200 * uiSettings.animationSpeed).toLong()
|
||||
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
|
||||
val typedValue = TypedValue()
|
||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSecondary,
|
||||
@@ -442,7 +457,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
binding.mediaBanner.pause()
|
||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
@@ -454,8 +468,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
.start()
|
||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.mediaBanner.resume()
|
||||
}
|
||||
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
|
||||
false
|
||||
@@ -479,9 +492,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
init {
|
||||
enabled(true)
|
||||
scope.launch {
|
||||
clicked()
|
||||
}
|
||||
image.setOnClickListener {
|
||||
if (pressable && !disabled) {
|
||||
pressable = false
|
||||
@@ -532,5 +542,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
image.alpha = if (disabled) 0.33f else 1f
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var mediaSingleton: Media? = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.FragmentManager
|
||||
@@ -11,7 +9,6 @@ import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
@@ -28,35 +25,32 @@ import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MediaDetailsViewModel : ViewModel() {
|
||||
val scrolledToTop = MutableLiveData(true)
|
||||
|
||||
fun saveSelected(id: Int, data: Selected, activity: Activity? = null) {
|
||||
saveData("$id-select", data, activity)
|
||||
fun saveSelected(id: Int, data: Selected) {
|
||||
PrefManager.setCustomVal("Selected-$id", data)
|
||||
}
|
||||
|
||||
|
||||
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
|
||||
val sharedPreferences = Injekt.get<SharedPreferences>()
|
||||
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
|
||||
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
|
||||
true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
|
||||
else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
|
||||
}
|
||||
it.preferDub = loadData("settings_prefer_dub") ?: false
|
||||
saveSelected(media.id, it)
|
||||
it
|
||||
}
|
||||
val data =
|
||||
PrefManager.getNullableCustomVal("Selected-${media.id}", null, Selected::class.java)
|
||||
?: Selected().let {
|
||||
it.sourceIndex = 0
|
||||
it.preferDub = PrefManager.getVal(PrefName.SettingsPreferDub)
|
||||
saveSelected(media.id, it)
|
||||
it
|
||||
}
|
||||
if (isDownload) {
|
||||
data.sourceIndex = if (media.anime != null) {
|
||||
AnimeSources.list.size - 1
|
||||
@@ -242,7 +236,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
i: String,
|
||||
manager: FragmentManager,
|
||||
launch: Boolean = true,
|
||||
prevEp: String? = null
|
||||
prevEp: String? = null,
|
||||
isDownload: Boolean = false
|
||||
) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
|
||||
@@ -254,13 +249,17 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
media.selected = this.loadSelected(media)
|
||||
val selector =
|
||||
SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
|
||||
SelectorDialogFragment.newInstance(
|
||||
media.selected!!.server,
|
||||
launch,
|
||||
prevEp,
|
||||
isDownload
|
||||
)
|
||||
selector.show(manager, "dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Manga
|
||||
var mangaReadSources: MangaReadSources? = null
|
||||
|
||||
@@ -314,7 +313,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
val novelSources = NovelSources
|
||||
val novelResponses = MutableLiveData<List<ShowResponse>>(null)
|
||||
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) {
|
||||
if (source != null) {
|
||||
novelResponses.postValue(source.search(query))
|
||||
|
||||
@@ -25,6 +25,8 @@ import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||
import ani.dantotsu.databinding.*
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -59,6 +61,7 @@ class MediaInfoFragment : Fragment() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val model: MediaDetailsViewModel by activityViewModels()
|
||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
binding.mediaInfoProgressBar.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 }
|
||||
@@ -92,8 +95,31 @@ class MediaInfoFragment : Fragment() {
|
||||
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
||||
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
||||
if (media.anime != null) {
|
||||
binding.mediaInfoDuration.text =
|
||||
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
|
||||
val episodeDuration = media.anime.episodeDuration
|
||||
|
||||
binding.mediaInfoDuration.text = when {
|
||||
episodeDuration != null -> {
|
||||
val hours = episodeDuration / 60
|
||||
val minutes = episodeDuration % 60
|
||||
|
||||
val formattedDuration = buildString {
|
||||
if (hours > 0) {
|
||||
append("$hours hour")
|
||||
if (hours > 1) append("s")
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
if (hours > 0) append(", ")
|
||||
append("$minutes min")
|
||||
if (minutes > 1) append("s")
|
||||
}
|
||||
}
|
||||
|
||||
formattedDuration
|
||||
}
|
||||
|
||||
else -> "??"
|
||||
}
|
||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeason.text =
|
||||
@@ -101,29 +127,33 @@ class MediaInfoFragment : Fragment() {
|
||||
if (media.anime.mainStudio != null) {
|
||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, StudioActivity::class.java).putExtra(
|
||||
"studio",
|
||||
media.anime.mainStudio!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, StudioActivity::class.java).putExtra(
|
||||
"studio",
|
||||
media.anime.mainStudio!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (media.anime.author != null) {
|
||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoAuthor.text = media.anime.author!!.name
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.anime.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.anime.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||
@@ -137,15 +167,17 @@ class MediaInfoFragment : Fragment() {
|
||||
if (media.manga.author != null) {
|
||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoAuthor.text = media.manga.author!!.name
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.manga.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.manga.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +221,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (media.trailer != null) {
|
||||
if (media.trailer != null && !offline) {
|
||||
@Suppress("DEPRECATION")
|
||||
class MyChrome : WebChromeClient() {
|
||||
private var mCustomView: View? = null
|
||||
@@ -243,7 +275,7 @@ class MediaInfoFragment : Fragment() {
|
||||
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())
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
|
||||
@@ -304,7 +336,7 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
if (media.genres.isNotEmpty()) {
|
||||
if (media.genres.isNotEmpty() && !offline) {
|
||||
val bind = ActivityGenreBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
@@ -335,7 +367,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (media.tags.isNotEmpty()) {
|
||||
if (media.tags.isNotEmpty() && !offline) {
|
||||
val bind = ItemTitleChipgroupBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
@@ -376,7 +408,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.characters.isNullOrEmpty()) {
|
||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
@@ -393,7 +425,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.relations.isNullOrEmpty()) {
|
||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||
if (media.sequel != null || media.prequel != null) {
|
||||
val bind = ItemQuelsBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
@@ -456,7 +488,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bindi.root)
|
||||
}
|
||||
|
||||
if (!media.recommendations.isNullOrEmpty()) {
|
||||
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
|
||||
@@ -16,7 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -196,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
media?.inCustomListsOf?.forEach {
|
||||
SwitchMaterial(requireContext()).apply {
|
||||
MaterialSwitch(requireContext()).apply {
|
||||
isChecked = it.value
|
||||
text = it.key
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
|
||||
@@ -16,7 +16,8 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -33,13 +34,14 @@ class SearchActivity : AppCompatActivity() {
|
||||
private lateinit var mediaAdaptor: MediaAdaptor
|
||||
private lateinit var progressAdapter: ProgressAdapter
|
||||
private lateinit var concatAdapter: ConcatAdapter
|
||||
private lateinit var headerAdaptor: SearchAdapter
|
||||
|
||||
lateinit var result: SearchResults
|
||||
lateinit var updateChips: (() -> Unit)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivitySearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -51,7 +53,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
bottom = navBarHeight + 80f.px
|
||||
)
|
||||
|
||||
style = loadData<Int>("searchStyle") ?: 0
|
||||
style = PrefManager.getVal(PrefName.SearchStyle)
|
||||
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
|
||||
if (!listOnly!!) listOnly = null
|
||||
|
||||
@@ -76,9 +78,9 @@ class SearchActivity : AppCompatActivity() {
|
||||
|
||||
progressAdapter = ProgressAdapter(searched = model.searched)
|
||||
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
|
||||
val headerAdaptor = SearchAdapter(this)
|
||||
headerAdaptor = SearchAdapter(this, model.searchResults.type)
|
||||
|
||||
val gridSize = (screenWidth / 124f).toInt()
|
||||
val gridSize = (screenWidth / 120f).toInt()
|
||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
@@ -154,12 +156,21 @@ class SearchActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun emptyMediaAdapter() {
|
||||
searchTimer.cancel()
|
||||
searchTimer.purge()
|
||||
mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size)
|
||||
model.searchResults.results.clear()
|
||||
progressAdapter.bar?.visibility = View.GONE
|
||||
}
|
||||
|
||||
private var searchTimer = Timer()
|
||||
private var loading = false
|
||||
fun search() {
|
||||
headerAdaptor.setHistoryVisibility(false)
|
||||
val size = model.searchResults.results.size
|
||||
model.searchResults.results.clear()
|
||||
runOnUiThread {
|
||||
binding.searchRecyclerView.post {
|
||||
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
@@ -188,6 +199,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
|
||||
var state: Parcelable? = null
|
||||
override fun onPause() {
|
||||
headerAdaptor.addHistory()
|
||||
super.onPause()
|
||||
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
@@ -1,31 +1,45 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.databinding.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.android.material.checkbox.MaterialCheckBox.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class SearchAdapter(private val activity: SearchActivity) :
|
||||
class SearchAdapter(private val activity: SearchActivity, private val type: String) :
|
||||
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
||||
private val itemViewType = 6969
|
||||
var search: Runnable? = null
|
||||
var requestFocus: Runnable? = null
|
||||
private var textWatcher: TextWatcher? = null
|
||||
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
||||
private lateinit var binding: ItemSearchHeaderBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||
val binding =
|
||||
@@ -35,8 +49,13 @@ class SearchAdapter(private val activity: SearchActivity) :
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
binding = holder.binding
|
||||
|
||||
searchHistoryAdapter = SearchHistoryAdapter(type) {
|
||||
binding.searchBarText.setText(it)
|
||||
}
|
||||
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
|
||||
binding.searchHistoryList.adapter = searchHistoryAdapter
|
||||
|
||||
val imm: InputMethodManager =
|
||||
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
@@ -54,6 +73,13 @@ class SearchAdapter(private val activity: SearchActivity) :
|
||||
}
|
||||
|
||||
binding.searchBar.hint = activity.result.type
|
||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||
val startIconDrawableRes = R.drawable.ic_incognito_24
|
||||
val startIconDrawable: Drawable? =
|
||||
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
|
||||
binding.searchBar.startIconDrawable = startIconDrawable
|
||||
}
|
||||
|
||||
var adult = activity.result.isAdult
|
||||
var listOnly = activity.result.onList
|
||||
|
||||
@@ -80,6 +106,9 @@ class SearchAdapter(private val activity: SearchActivity) :
|
||||
onList = listOnly
|
||||
isAdult = adult
|
||||
}
|
||||
if (binding.searchBarText.text.toString().equals("hentai", true)) {
|
||||
openLinkInBrowser("https://www.youtube.com/watch?v=GgJrEOo0QoA")
|
||||
}
|
||||
activity.search()
|
||||
}
|
||||
|
||||
@@ -89,7 +118,18 @@ class SearchAdapter(private val activity: SearchActivity) :
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
searchTitle()
|
||||
if (s.toString().isBlank()) {
|
||||
activity.emptyMediaAdapter()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(200)
|
||||
activity.runOnUiThread {
|
||||
setHistoryVisibility(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setHistoryVisibility(false)
|
||||
searchTitle()
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.searchBarText.addTextChangedListener(textWatcher)
|
||||
@@ -112,14 +152,14 @@ class SearchAdapter(private val activity: SearchActivity) :
|
||||
it.alpha = 1f
|
||||
binding.searchResultList.alpha = 0.33f
|
||||
activity.style = 0
|
||||
saveData("searchStyle", 0)
|
||||
PrefManager.setVal(PrefName.SearchStyle, 0)
|
||||
activity.recycler()
|
||||
}
|
||||
binding.searchResultList.setOnClickListener {
|
||||
it.alpha = 1f
|
||||
binding.searchResultGrid.alpha = 0.33f
|
||||
activity.style = 1
|
||||
saveData("searchStyle", 1)
|
||||
PrefManager.setVal(PrefName.SearchStyle, 1)
|
||||
activity.recycler()
|
||||
}
|
||||
|
||||
@@ -162,6 +202,40 @@ class SearchAdapter(private val activity: SearchActivity) :
|
||||
requestFocus = Runnable { binding.searchBarText.requestFocus() }
|
||||
}
|
||||
|
||||
fun setHistoryVisibility(visible: Boolean) {
|
||||
if (visible) {
|
||||
binding.searchResultLayout.startAnimation(fadeOutAnimation())
|
||||
binding.searchHistoryList.startAnimation(fadeInAnimation())
|
||||
binding.searchResultLayout.visibility = View.GONE
|
||||
binding.searchHistoryList.visibility = View.VISIBLE
|
||||
} else {
|
||||
if (binding.searchResultLayout.visibility != View.VISIBLE) {
|
||||
binding.searchResultLayout.startAnimation(fadeInAnimation())
|
||||
binding.searchHistoryList.startAnimation(fadeOutAnimation())
|
||||
}
|
||||
binding.searchResultLayout.visibility = View.VISIBLE
|
||||
binding.searchHistoryList.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun fadeInAnimation(): Animation {
|
||||
return AlphaAnimation(0f, 1f).apply {
|
||||
duration = 150
|
||||
fillAfter = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun fadeOutAnimation(): Animation {
|
||||
return AlphaAnimation(1f, 0f).apply {
|
||||
duration = 150
|
||||
fillAfter = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun addHistory() {
|
||||
searchHistoryAdapter.add(binding.searchBarText.text.toString())
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import com.google.android.material.chip.Chip
|
||||
import java.util.Calendar
|
||||
|
||||
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSearchFilterBinding? = null
|
||||
@@ -103,7 +104,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
(1970 until 2025).map { it.toString() }.reversed().toTypedArray()
|
||||
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
|
||||
.reversed().toTypedArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
100
app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt
Normal file
100
app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt
Normal file
@@ -0,0 +1,100 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemSearchHistoryBinding
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.SharedPreferenceStringSetLiveData
|
||||
import java.util.Locale
|
||||
|
||||
class SearchHistoryAdapter(private val type: String, private val searchClicked: (String) -> Unit) :
|
||||
ListAdapter<String, SearchHistoryAdapter.SearchHistoryViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
) {
|
||||
private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null
|
||||
private var searchHistory: MutableSet<String>? = null
|
||||
private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) {
|
||||
"anime" -> PrefName.AnimeSearchHistory
|
||||
"manga" -> PrefName.MangaSearchHistory
|
||||
else -> throw IllegalArgumentException("Invalid type")
|
||||
}
|
||||
|
||||
init {
|
||||
searchHistoryLiveData =
|
||||
PrefManager.getLiveVal(historyType, mutableSetOf<String>()).asLiveStringSet()
|
||||
searchHistoryLiveData?.observeForever {
|
||||
searchHistory = it.toMutableSet()
|
||||
submitList(searchHistory?.toList())
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(item: String) {
|
||||
searchHistory?.remove(item)
|
||||
PrefManager.setVal(historyType, searchHistory)
|
||||
submitList(searchHistory?.toList())
|
||||
}
|
||||
|
||||
fun add(item: String) {
|
||||
if (searchHistory?.contains(item) == true || item.isBlank()) return
|
||||
if (PrefManager.getVal(PrefName.Incognito)) return
|
||||
searchHistory?.add(item)
|
||||
submitList(searchHistory?.toList())
|
||||
PrefManager.setVal(historyType, searchHistory)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): SearchHistoryAdapter.SearchHistoryViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_search_history, parent, false)
|
||||
return SearchHistoryViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: SearchHistoryAdapter.SearchHistoryViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
val item = getItem(position)
|
||||
holder.binding.searchHistoryTextView.text = item
|
||||
holder.binding.closeTextView.setOnClickListener {
|
||||
val currentPosition = holder.bindingAdapterPosition
|
||||
if (currentPosition >= itemCount || currentPosition < 0) return@setOnClickListener
|
||||
remove(getItem(currentPosition))
|
||||
}
|
||||
holder.binding.searchHistoryTextView.setOnClickListener {
|
||||
val currentPosition = holder.bindingAdapterPosition
|
||||
if (currentPosition >= itemCount || currentPosition < 0) return@setOnClickListener
|
||||
searchClicked(getItem(currentPosition))
|
||||
}
|
||||
}
|
||||
|
||||
inner class SearchHistoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val binding = ItemSearchHistoryBinding.bind(view)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFF_CALLBACK_INSTALLED = object : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: String,
|
||||
newItem: String
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: String,
|
||||
newItem: String
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import ani.dantotsu.Refresh
|
||||
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.px
|
||||
import ani.dantotsu.statusBarHeight
|
||||
@@ -36,7 +35,7 @@ class StudioActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityStudioBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
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 downloadSubtitles(context: Context, url: String): SubtitleType =
|
||||
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>()
|
||||
@@ -29,8 +33,8 @@ class SubtitleDownloader {
|
||||
|
||||
|
||||
val subtitleType = when {
|
||||
responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS
|
||||
responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT
|
||||
responseBody.contains("[Script Info]") -> SubtitleType.ASS
|
||||
responseBody.contains("WEBVTT") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
}
|
||||
|
||||
@@ -39,5 +43,50 @@ class SubtitleDownloader {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,68 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import java.util.Locale
|
||||
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 = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
||||
|
||||
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
||||
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val soft = subdubMatcher.group(1)
|
||||
val subdub = subdubMatcher.group(2)
|
||||
val bed = subdubMatcher.group(3) ?: ""
|
||||
|
||||
val toggled = when (typeToSetTo) {
|
||||
SubDubType.SUB -> "sub"
|
||||
SubDubType.DUB -> "dub"
|
||||
SubDubType.NULL -> ""
|
||||
}
|
||||
val toggledCasePreserved =
|
||||
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
|
||||
?.isUpperCase() == true
|
||||
) toggled.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(
|
||||
Locale.ROOT
|
||||
) else it.toString()
|
||||
} else toggled
|
||||
|
||||
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubDub(text: String): SubDubType {
|
||||
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
|
||||
when (subdub) {
|
||||
"sub" -> SubDubType.SUB
|
||||
"dub" -> SubDubType.DUB
|
||||
else -> SubDubType.NULL
|
||||
}
|
||||
} else {
|
||||
SubDubType.NULL
|
||||
}
|
||||
}
|
||||
|
||||
enum class SubDubType {
|
||||
SUB, DUB, NULL
|
||||
}
|
||||
|
||||
fun findSeasonNumber(text: String): Int? {
|
||||
val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)"
|
||||
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||
|
||||
@@ -16,5 +72,56 @@ class AnimeNameAdapter {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,36 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.ImageButton
|
||||
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.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
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.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
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.launch
|
||||
|
||||
|
||||
class AnimeWatchAdapter(
|
||||
private val media: Media,
|
||||
private val fragment: AnimeWatchFragment,
|
||||
@@ -41,13 +51,16 @@ class AnimeWatchAdapter(
|
||||
return ViewHolder(bind)
|
||||
}
|
||||
|
||||
private var nestedDialog: AlertDialog? = null
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
_binding = binding
|
||||
|
||||
//Youtube
|
||||
if (media.anime!!.youtube != null && fragment.uiSettings.showYtButton) {
|
||||
if (media.anime!!.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
|
||||
binding.animeSourceYT.visibility = View.VISIBLE
|
||||
binding.animeSourceYT.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
|
||||
@@ -78,6 +91,15 @@ class AnimeWatchAdapter(
|
||||
null
|
||||
)
|
||||
}
|
||||
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal(
|
||||
PrefName.OfflineMode
|
||||
)
|
||||
) View.GONE else View.VISIBLE
|
||||
|
||||
binding.animeSourceNameContainer.visibility = offline
|
||||
binding.animeSourceSettings.visibility = offline
|
||||
binding.animeSourceSearch.visibility = offline
|
||||
binding.animeSourceTitle.visibility = offline
|
||||
|
||||
//Source Selection
|
||||
var source =
|
||||
@@ -90,7 +112,7 @@ class AnimeWatchAdapter(
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +132,7 @@ class AnimeWatchAdapter(
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
source = i
|
||||
setLanguageList(0, i)
|
||||
}
|
||||
@@ -131,7 +153,7 @@ class AnimeWatchAdapter(
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
setLanguageList(i, source)
|
||||
}
|
||||
subscribeButton(false)
|
||||
@@ -147,8 +169,9 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
//Icons
|
||||
|
||||
//Subscription
|
||||
//subscribe
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
binding.animeSourceSubscribe,
|
||||
@@ -167,44 +190,107 @@ class AnimeWatchAdapter(
|
||||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||
}
|
||||
|
||||
//Icons
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
binding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
var selected = when (style) {
|
||||
0 -> binding.animeSourceList
|
||||
1 -> binding.animeSourceGrid
|
||||
2 -> binding.animeSourceCompact
|
||||
else -> binding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
fun selected(it: ImageView) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
//Nested Button
|
||||
binding.animeNestedButton.setOnClickListener {
|
||||
val dialogView =
|
||||
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||
var refresh = false
|
||||
var run = false
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style =
|
||||
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
dialogBinding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
run = true
|
||||
}
|
||||
//Grids
|
||||
var selected = when (style) {
|
||||
0 -> dialogBinding.animeSourceList
|
||||
1 -> dialogBinding.animeSourceGrid
|
||||
2 -> dialogBinding.animeSourceCompact
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> dialogBinding.layoutText.text = "List"
|
||||
1 -> dialogBinding.layoutText.text = "Grid"
|
||||
2 -> dialogBinding.layoutText.text = "Compact"
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
fun selected(it: ImageButton) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
dialogBinding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 0
|
||||
dialogBinding.layoutText.text = "List"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceGrid.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 1
|
||||
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 headersMap = try {
|
||||
sourceHttp.headers.toMultimap()
|
||||
.mapValues { it.value.getOrNull(0) ?: "" }
|
||||
} catch (e: Exception) {
|
||||
emptyMap()
|
||||
}
|
||||
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
.putExtra("headers", headersMap as HashMap<String, String>)
|
||||
startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//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()
|
||||
}
|
||||
binding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeSourceGrid.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 2
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeScanlatorTop.visibility = View.GONE
|
||||
binding.animeDownloadTop.visibility = View.GONE
|
||||
//Episode Handling
|
||||
handleEpisodes()
|
||||
}
|
||||
@@ -278,7 +364,9 @@ class AnimeWatchAdapter(
|
||||
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
||||
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp = loadData<String>("${media.id}_current_ep")?.toIntOrNull() ?: 1
|
||||
val appEp =
|
||||
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
|
||||
?: 1
|
||||
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
if (episodes.contains(continueEp)) {
|
||||
@@ -290,7 +378,10 @@ class AnimeWatchAdapter(
|
||||
media.id,
|
||||
continueEp
|
||||
)
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > fragment.playerSettings.watchPercentage) {
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > PrefManager.getVal<Float>(
|
||||
PrefName.WatchPercentage
|
||||
)
|
||||
) {
|
||||
val e = episodes.indexOf(continueEp)
|
||||
if (e != -1 && e + 1 < episodes.size) {
|
||||
continueEp = episodes[e + 1]
|
||||
@@ -304,17 +395,23 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
val ep = media.anime.episodes!![continueEp]!!
|
||||
|
||||
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
|
||||
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 {
|
||||
fragment.onEpisodeClick(continueEp)
|
||||
}
|
||||
if (fragment.continueEp) {
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < fragment.playerSettings.watchPercentage) {
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
|
||||
PrefName.WatchPercentage
|
||||
)
|
||||
) {
|
||||
binding.animeSourceContinue.performClick()
|
||||
fragment.continueEp = false
|
||||
}
|
||||
@@ -322,6 +419,7 @@ class AnimeWatchAdapter(
|
||||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.animeSourceProgressBar.visibility = View.GONE
|
||||
if (media.anime.episodes!!.isNotEmpty())
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
@@ -336,7 +434,7 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguageList(lang: Int, source: Int) {
|
||||
private fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (watchSources is AnimeSources) {
|
||||
val parser = watchSources[source] as? DynamicAnimeParser
|
||||
@@ -351,12 +449,16 @@ class AnimeWatchAdapter(
|
||||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
binding?.animeSourceLanguage?.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { it.lang })
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -371,4 +473,4 @@ class AnimeWatchAdapter(
|
||||
countDown(media, binding.animeSourceContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
@@ -9,26 +13,36 @@ import android.view.View
|
||||
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.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
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.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
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.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.AnimeParser
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.settings.PlayerSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.subcriptions.Notifications
|
||||
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
@@ -42,6 +56,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
@@ -61,14 +77,14 @@ class AnimeWatchFragment : Fragment() {
|
||||
private lateinit var headerAdapter: AnimeWatchAdapter
|
||||
private lateinit var episodeAdapter: EpisodeAdapter
|
||||
|
||||
val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
var screenWidth = 0f
|
||||
private var progress = View.VISIBLE
|
||||
|
||||
var continueEp: Boolean = false
|
||||
var loaded = false
|
||||
|
||||
lateinit var playerSettings: PlayerSettings
|
||||
lateinit var uiSettings: UserInterfaceSettings
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -80,18 +96,27 @@ class AnimeWatchFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
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)
|
||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||
|
||||
var maxGridSize = (screenWidth / 100f).roundToInt()
|
||||
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
||||
|
||||
playerSettings =
|
||||
loadData("player_settings", toast = false)
|
||||
?: PlayerSettings().apply { saveData("player_settings", this) }
|
||||
uiSettings = loadData("ui_settings", toast = false)
|
||||
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||
|
||||
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
|
||||
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
@@ -112,6 +137,23 @@ class AnimeWatchFragment : Fragment() {
|
||||
|
||||
binding.animeSourceRecycler.layoutManager = gridLayoutManager
|
||||
|
||||
binding.ScrollTop.setOnClickListener {
|
||||
binding.animeSourceRecycler.scrollToPosition(10)
|
||||
binding.animeSourceRecycler.smoothScrollToPosition(0)
|
||||
}
|
||||
binding.animeSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val position = gridLayoutManager.findFirstVisibleItemPosition()
|
||||
if (position > 2) {
|
||||
binding.ScrollTop.translationY = -navBarHeight.toFloat()
|
||||
binding.ScrollTop.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.ScrollTop.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
})
|
||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||
if (it) binding.animeSourceRecycler.scrollToPosition(0)
|
||||
}
|
||||
@@ -123,7 +165,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
media.selected = model.loadSelected(media)
|
||||
|
||||
subscribed =
|
||||
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
||||
SubscriptionHelper.getSubscriptions().containsKey(media.id)
|
||||
|
||||
style = media.selected!!.recyclerStyle
|
||||
reverse = media.selected!!.recyclerReversed
|
||||
@@ -134,9 +176,17 @@ class AnimeWatchFragment : Fragment() {
|
||||
if (!loaded) {
|
||||
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
||||
|
||||
val offlineMode =
|
||||
model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
|
||||
|
||||
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
|
||||
episodeAdapter =
|
||||
EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
|
||||
EpisodeAdapter(
|
||||
style ?: PrefManager.getVal(PrefName.AnimeDefaultView),
|
||||
media,
|
||||
this,
|
||||
offlineMode = offlineMode
|
||||
)
|
||||
|
||||
binding.animeSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||
@@ -169,12 +219,15 @@ class AnimeWatchFragment : Fragment() {
|
||||
if (media.anime?.kitsuEpisodes != null) {
|
||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||
episode.desc =
|
||||
episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
|
||||
episode.title =
|
||||
episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
|
||||
episode.thumb =
|
||||
episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb
|
||||
?: FileUrl[media.cover]
|
||||
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +283,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null
|
||||
selected.sourceIndex = i
|
||||
selected.server = null
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
media.selected = selected
|
||||
return model.watchSources?.get(i)!!
|
||||
}
|
||||
@@ -238,7 +291,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
fun onLangChange(i: Int) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.langIndex = i
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
@@ -246,7 +299,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
val selected = model.loadSelected(media)
|
||||
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
||||
selected.preferDub = checked
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
media.selected = selected
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.forceLoadEpisode(
|
||||
@@ -265,7 +318,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
reverse = rev
|
||||
media.selected!!.recyclerStyle = style
|
||||
media.selected!!.recyclerReversed = reverse
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
model.saveSelected(media.id, media.selected!!)
|
||||
reload()
|
||||
}
|
||||
|
||||
@@ -273,7 +326,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
media.selected!!.chip = i
|
||||
start = s
|
||||
end = e
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
model.saveSelected(media.id, media.selected!!)
|
||||
reload()
|
||||
}
|
||||
|
||||
@@ -298,33 +351,34 @@ class AnimeWatchFragment : Fragment() {
|
||||
|
||||
fun openSettings(pkg: AnimeExtension.Installed) {
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as MediaDetailsActivity
|
||||
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
|
||||
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
|
||||
}
|
||||
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 { it.lang }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
val names =
|
||||
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
.setSingleChoiceItems(names, -1) { dialog, which ->
|
||||
selectedSetting = allSettings[which]
|
||||
itemSelected = true
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
@@ -341,10 +395,10 @@ class AnimeWatchFragment : Fragment() {
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
changeUIVisibility(true)
|
||||
return@setNegativeButton
|
||||
.setOnDismissListener {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
@@ -373,10 +427,91 @@ class AnimeWatchFragment : Fragment() {
|
||||
|
||||
fun onEpisodeClick(i: String) {
|
||||
model.continueMedia = false
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
model.saveSelected(media.id, media.selected!!)
|
||||
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 = PrefManager.getAnimeDownloadPreferences().getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
PrefManager.getAnimeDownloadPreferences().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")
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
@@ -387,8 +522,10 @@ class AnimeWatchFragment : Fragment() {
|
||||
selected.latest =
|
||||
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
headerAdapter.handleEpisodes()
|
||||
val isDownloaded = model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
|
||||
episodeAdapter.offlineMode = isDownloaded
|
||||
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
|
||||
var arr: ArrayList<Episode> = arrayListOf()
|
||||
if (media.anime!!.episodes != null) {
|
||||
@@ -401,13 +538,22 @@ class AnimeWatchFragment : Fragment() {
|
||||
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
|
||||
}
|
||||
episodeAdapter.arr = arr
|
||||
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
|
||||
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
for (download in downloadManager.animeDownloadedTypes) {
|
||||
if (download.title == media.mainName()) {
|
||||
episodeAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
model.watchSources?.flushText()
|
||||
super.onDestroy()
|
||||
try {
|
||||
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
var state: Parcelable? = null
|
||||
@@ -422,4 +568,12 @@ class AnimeWatchFragment : Fragment() {
|
||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
}
|
||||
companion object {
|
||||
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
|
||||
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
|
||||
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
|
||||
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
|
||||
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.mediarouter.app.MediaRouteActionProvider
|
||||
import androidx.mediarouter.app.MediaRouteChooserDialog
|
||||
import androidx.mediarouter.app.MediaRouteChooserDialogFragment
|
||||
import androidx.mediarouter.app.MediaRouteControllerDialog
|
||||
import androidx.mediarouter.app.MediaRouteControllerDialogFragment
|
||||
import androidx.mediarouter.app.MediaRouteDialogFactory
|
||||
import ani.dantotsu.R
|
||||
|
||||
class CustomCastProvider(context: Context) : MediaRouteActionProvider(context) {
|
||||
init {
|
||||
dialogFactory = CustomCastThemeFactory()
|
||||
}
|
||||
}
|
||||
|
||||
class CustomCastThemeFactory : MediaRouteDialogFactory() {
|
||||
override fun onCreateChooserDialogFragment(): MediaRouteChooserDialogFragment {
|
||||
return CustomMediaRouterChooserDialogFragment()
|
||||
}
|
||||
|
||||
override fun onCreateControllerDialogFragment(): MediaRouteControllerDialogFragment {
|
||||
return CustomMediaRouteControllerDialogFragment()
|
||||
}
|
||||
}
|
||||
|
||||
class CustomMediaRouterChooserDialogFragment : MediaRouteChooserDialogFragment() {
|
||||
override fun onCreateChooserDialog(
|
||||
context: Context,
|
||||
savedInstanceState: Bundle?
|
||||
): MediaRouteChooserDialog =
|
||||
MediaRouteChooserDialog(context, R.style.MyPopup)
|
||||
}
|
||||
|
||||
class CustomMediaRouteControllerDialogFragment : MediaRouteControllerDialogFragment() {
|
||||
override fun onCreateControllerDialog(
|
||||
context: Context,
|
||||
savedInstanceState: Bundle?
|
||||
): MediaRouteControllerDialog =
|
||||
MediaRouteControllerDialog(context, R.style.MyPopup)
|
||||
}
|
||||
@@ -14,7 +14,8 @@ data class Episode(
|
||||
var selectedExtractor: String? = null,
|
||||
var selectedVideo: Int = 0,
|
||||
var selectedSubtitle: Int? = -1,
|
||||
var extractors: MutableList<VideoExtractor>? = null,
|
||||
var downloadProgress: String? = null,
|
||||
@Transient var extractors: MutableList<VideoExtractor>? = null,
|
||||
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
|
||||
var allStreams: Boolean = false,
|
||||
var watched: Long? = null,
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadIndex
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||
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.settings.saving.PrefManager
|
||||
import com.bumptech.glide.Glide
|
||||
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) {
|
||||
val curr = loadData<Long>("${mediaId}_${ep}")
|
||||
val max = loadData<Long>("${mediaId}_${ep}_max")
|
||||
val curr = PrefManager.getNullableCustomVal("${mediaId}_${ep}", null, Long::class.java)
|
||||
val max = PrefManager.getNullableCustomVal("${mediaId}_${ep}_max", null, Long::class.java)
|
||||
if (curr != null && max != null) {
|
||||
cont.visibility = View.VISIBLE
|
||||
val div = curr.toFloat() / max.toFloat()
|
||||
@@ -32,13 +45,24 @@ fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep:
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class EpisodeAdapter(
|
||||
private var type: Int,
|
||||
private val media: Media,
|
||||
private val fragment: AnimeWatchFragment,
|
||||
var arr: List<Episode> = arrayListOf()
|
||||
var arr: List<Episode> = arrayListOf(),
|
||||
var offlineMode: Boolean
|
||||
) : 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 {
|
||||
return (when (viewType) {
|
||||
0 -> EpisodeListViewHolder(
|
||||
@@ -76,24 +100,23 @@ class EpisodeAdapter(
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val ep = arr[position]
|
||||
val title =
|
||||
"${
|
||||
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}"
|
||||
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
|
||||
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||
} else {
|
||||
ep.number
|
||||
} ?: ""
|
||||
|
||||
when (holder) {
|
||||
is EpisodeListViewHolder -> {
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
|
||||
val thumb =
|
||||
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
||||
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
|
||||
.into(binding.itemEpisodeImage)
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeTitle.text = title
|
||||
binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
|
||||
|
||||
if (ep.filler) {
|
||||
binding.itemEpisodeFiller.visibility = View.VISIBLE
|
||||
@@ -105,6 +128,7 @@ class EpisodeAdapter(
|
||||
binding.itemEpisodeDesc.visibility =
|
||||
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
|
||||
binding.itemEpisodeDesc.text = ep.desc ?: ""
|
||||
holder.bind(ep.number, ep.downloadProgress, ep.desc)
|
||||
|
||||
if (media.userProgress != null) {
|
||||
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) {
|
||||
@@ -134,7 +158,7 @@ class EpisodeAdapter(
|
||||
|
||||
is EpisodeGridViewHolder -> {
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
|
||||
val thumb =
|
||||
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
|
||||
@@ -177,7 +201,7 @@ class EpisodeAdapter(
|
||||
|
||||
is EpisodeCompactViewHolder -> {
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeFillerView.visibility =
|
||||
if (ep.filler) View.VISIBLE else View.GONE
|
||||
@@ -205,6 +229,77 @@ class EpisodeAdapter(
|
||||
|
||||
override fun getItemCount(): Int = arr.size
|
||||
|
||||
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 = PrefManager.getAnimeDownloadPreferences().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 {
|
||||
@@ -227,11 +322,36 @@ class EpisodeAdapter(
|
||||
|
||||
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private val activeCoroutines = mutableSetOf<String>()
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
|
||||
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 {
|
||||
if (binding.itemEpisodeDesc.maxLines == 3)
|
||||
binding.itemEpisodeDesc.maxLines = 100
|
||||
@@ -239,11 +359,79 @@ class EpisodeAdapter(
|
||||
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.itemDownload.rotation = 0f
|
||||
}
|
||||
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.postDelayed({
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
|
||||
binding.itemDownload.rotation = 0f
|
||||
}, 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_download_24)
|
||||
binding.itemDownload.rotation = 0f
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startOrContinueRotation(episodeNumber: String, resetRotation: () -> Unit) {
|
||||
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)
|
||||
resetRotation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRotationCoroutineRunningFor(episodeNumber: String): Boolean {
|
||||
return episodeNumber in activeCoroutines
|
||||
}
|
||||
}
|
||||
|
||||
fun updateType(t: Int) {
|
||||
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,13 +1,14 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -17,20 +18,28 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||
import ani.dantotsu.databinding.ItemStreamBinding
|
||||
import ani.dantotsu.databinding.ItemUrlBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.others.Download.download
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DecimalFormat
|
||||
|
||||
|
||||
class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSelectorBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
@@ -42,6 +51,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
private var makeDefault = false
|
||||
private var selected: String? = null
|
||||
private var launch: Boolean? = null
|
||||
private var isDownloadMenu: Boolean? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -49,6 +59,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
selected = it.getString("server")
|
||||
launch = it.getBoolean("launch", true)
|
||||
prevEpisode = it.getString("prev")
|
||||
isDownloadMenu = it.getBoolean("isDownload")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +87,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
|
||||
episode = ep
|
||||
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.selectorAutoListContainer.visibility = View.VISIBLE
|
||||
binding.selectorAutoText.text = selected
|
||||
binding.selectorCancel.setOnClickListener {
|
||||
media!!.selected!!.server = null
|
||||
model.saveSelected(media!!.id, media!!.selected!!, requireActivity())
|
||||
model.saveSelected(media!!.id, media!!.selected!!)
|
||||
tryWith {
|
||||
dismiss()
|
||||
}
|
||||
@@ -95,7 +109,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
fun load() {
|
||||
val size =
|
||||
ep.extractors?.find { it.server.name == selected }?.videos?.size
|
||||
if (model.watchSources!!.isDownloadedSource(media!!.selected!!.sourceIndex)) {
|
||||
ep.extractors?.firstOrNull()?.videos?.size
|
||||
} else {
|
||||
ep.extractors?.find { it.server.name == selected }?.videos?.size
|
||||
}
|
||||
|
||||
if (size != null && size >= media!!.selected!!.video) {
|
||||
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
|
||||
selected
|
||||
@@ -127,11 +146,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
binding.selectorRecyclerView.adapter = null
|
||||
binding.selectorProgressBar.visibility = View.VISIBLE
|
||||
makeDefault = loadData("make_default") ?: true
|
||||
makeDefault = PrefManager.getVal(PrefName.MakeDefault)
|
||||
binding.selectorMakeDefault.isChecked = makeDefault
|
||||
binding.selectorMakeDefault.setOnClickListener {
|
||||
makeDefault = binding.selectorMakeDefault.isChecked
|
||||
saveData("make_default", makeDefault)
|
||||
PrefManager.setVal(PrefName.MakeDefault, makeDefault)
|
||||
}
|
||||
binding.selectorRecyclerView.layoutManager =
|
||||
LinearLayoutManager(
|
||||
@@ -145,6 +164,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
ep.extractorCallback = {
|
||||
scope.launch {
|
||||
adapter.add(it)
|
||||
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
|
||||
adapter.performClick(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
model.getEpisode().observe(this) {
|
||||
@@ -164,6 +186,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
} else {
|
||||
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
|
||||
adapter.addAll(ep.extractors)
|
||||
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
|
||||
adapter.performClick(0)
|
||||
}
|
||||
binding.selectorProgressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -179,7 +204,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
prevEpisode = null
|
||||
|
||||
dismiss()
|
||||
if (launch!!) {
|
||||
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
||||
stopAddingToList()
|
||||
val intent = Intent(activity, ExoplayerView::class.java)
|
||||
ExoplayerView.media = media
|
||||
@@ -214,7 +239,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
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.adapter = VideoAdapter(extractor)
|
||||
@@ -235,6 +261,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
notifyItemRangeInserted(0, extractors.size)
|
||||
}
|
||||
|
||||
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) {
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class StreamViewHolder(val binding: ItemStreamBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
@@ -256,22 +294,98 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val video = extractor.videos[position]
|
||||
binding.urlQuality.text =
|
||||
if (video.quality != null) "${video.quality}p" else "Default Quality"
|
||||
binding.urlNote.text = video.extraNote ?: ""
|
||||
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
|
||||
binding.urlDownload.visibility = View.VISIBLE
|
||||
if (isDownloadMenu == true) {
|
||||
binding.urlDownload.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.urlDownload.visibility = View.GONE
|
||||
}
|
||||
binding.urlDownload.setSafeOnClickListener {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||
extractor.server.name
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
|
||||
position
|
||||
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
download(
|
||||
requireActivity(),
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
||||
media!!.userPreferredName
|
||||
)
|
||||
if ((PrefManager.getVal(PrefName.DownloadManager) as Int) != 0) {
|
||||
download(
|
||||
requireActivity(),
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
||||
media!!.userPreferredName
|
||||
)
|
||||
} else {
|
||||
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
||||
val selectedVideo =
|
||||
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||
val subtitles = extractor.subtitles
|
||||
val subtitleNames = subtitles.map { it.language }
|
||||
var subtitleToDownload: Subtitle? = null
|
||||
val activity = currActivity()?:requireActivity()
|
||||
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(
|
||||
activity,
|
||||
media!!.mainName(),
|
||||
episode.number,
|
||||
selectedVideo,
|
||||
subtitleToDownload,
|
||||
media,
|
||||
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
||||
)
|
||||
broadcastDownloadStarted(episode.number, activity)
|
||||
} 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
|
||||
)
|
||||
broadcastDownloadStarted(episode.number, activity)
|
||||
} 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
|
||||
)
|
||||
broadcastDownloadStarted(episode.number, activity)
|
||||
} else {
|
||||
snackString("No Video Selected")
|
||||
}
|
||||
}
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
if (video.format == VideoType.CONTAINER) {
|
||||
@@ -281,12 +395,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
|
||||
"#.##"
|
||||
).format(video.size ?: 0).toString() + " MB"))
|
||||
} else {
|
||||
binding.urlQuality.text = "Multi Quality"
|
||||
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
|
||||
}
|
||||
|
||||
private fun broadcastDownloadStarted(episodeNumber: String, activity: Activity) {
|
||||
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply {
|
||||
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
|
||||
}
|
||||
activity.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = extractor.videos.size
|
||||
@@ -295,6 +414,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setSafeOnClickListener {
|
||||
if (isDownloadMenu == true) {
|
||||
binding.urlDownload.performClick()
|
||||
return@setSafeOnClickListener
|
||||
}
|
||||
tryWith(true) {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
|
||||
extractor.server.name
|
||||
@@ -303,7 +426,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
if (makeDefault) {
|
||||
media!!.selected!!.server = extractor.server.name
|
||||
media!!.selected!!.video = bindingAdapterPosition
|
||||
model.saveSelected(media!!.id, media!!.selected!!, requireActivity())
|
||||
model.saveSelected(media!!.id, media!!.selected!!)
|
||||
}
|
||||
startExoplayer(media!!)
|
||||
}
|
||||
@@ -326,13 +449,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
fun newInstance(
|
||||
server: String? = null,
|
||||
la: Boolean = true,
|
||||
prev: String? = null
|
||||
prev: String? = null,
|
||||
isDownload: Boolean
|
||||
): SelectorDialogFragment =
|
||||
SelectorDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString("server", server)
|
||||
putBoolean("launch", la)
|
||||
putString("prev", prev)
|
||||
putBoolean("isDownload", isDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,18 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.BottomSheetSubtitlesBinding
|
||||
import ani.dantotsu.databinding.ItemSubtitleTextBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
|
||||
class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSubtitlesBinding? = null
|
||||
@@ -60,13 +61,14 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
)
|
||||
)
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
if (position == 0) {
|
||||
binding.subtitleTitle.setText(R.string.none)
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
val selSubs: String? = loadData("subLang_${mediaID}", activity)
|
||||
val selSubs = PrefManager.getCustomVal<String?>("subLang_${mediaID}", null)
|
||||
if (episode.selectedSubtitle != null && selSubs != "None") {
|
||||
binding.root.setCardBackgroundColor(TRANSPARENT)
|
||||
}
|
||||
@@ -76,7 +78,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
model.setEpisode(episode, "Subtitle")
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
saveData("subLang_${mediaID}", "None", activity)
|
||||
PrefManager.setCustomVal("subLang_${mediaID}", "None")
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
@@ -105,7 +107,8 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
val selSubs: String? = loadData("subLang_${mediaID}", activity)
|
||||
val selSubs: String? =
|
||||
PrefManager.getCustomVal<String?>("subLang_${mediaID}", null)
|
||||
if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) {
|
||||
binding.root.setCardBackgroundColor(TRANSPARENT)
|
||||
}
|
||||
@@ -116,7 +119,10 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
model.setEpisode(episode, "Subtitle")
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
saveData("subLang_${mediaID}", subtitles[position - 1].language, activity)
|
||||
PrefManager.setCustomVal(
|
||||
"subLang_${mediaID}",
|
||||
subtitles[position - 1].language
|
||||
)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ class MangaChapterAdapter(
|
||||
|
||||
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
|
||||
@@ -140,8 +141,8 @@ class MangaChapterAdapter(
|
||||
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private val activeCoroutines = mutableSetOf<String>()
|
||||
val typedValue1 = TypedValue()
|
||||
val typedValue2 = TypedValue()
|
||||
private val typedValue1 = TypedValue()
|
||||
private val typedValue2 = TypedValue()
|
||||
fun bind(chapterNumber: String, progress: String?) {
|
||||
if (progress != null) {
|
||||
binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
@@ -153,23 +154,25 @@ class MangaChapterAdapter(
|
||||
if (activeDownloads.contains(chapterNumber)) {
|
||||
// Show spinner
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_sync)
|
||||
startOrContinueRotation(chapterNumber)
|
||||
startOrContinueRotation(chapterNumber) {
|
||||
binding.itemDownload.rotation = 0f
|
||||
}
|
||||
} 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.setColorFilter(typedValue2.data)
|
||||
binding.itemDownload.rotation = 0f
|
||||
}, 1000)
|
||||
} else {
|
||||
// Show download icon
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_download_24)
|
||||
binding.itemDownload.rotation = 0f
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startOrContinueRotation(chapterNumber: String) {
|
||||
private fun startOrContinueRotation(chapterNumber: String, resetRotation: () -> Unit) {
|
||||
if (!isRotationCoroutineRunningFor(chapterNumber)) {
|
||||
val scope = fragment.lifecycle.coroutineScope
|
||||
scope.launch {
|
||||
@@ -184,6 +187,7 @@ class MangaChapterAdapter(
|
||||
}
|
||||
// Remove chapter number from active coroutines set
|
||||
activeCoroutines.remove(chapterNumber)
|
||||
resetRotation()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +219,16 @@ class MangaChapterAdapter(
|
||||
fragment.onMangaChapterStopDownloadClick(chapterNumber)
|
||||
return@setOnClickListener
|
||||
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||
fragment.onMangaChapterRemoveDownloadClick(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)
|
||||
@@ -233,7 +246,7 @@ class MangaChapterAdapter(
|
||||
input.maxValue = itemCount - bindingAdapterPosition
|
||||
input.value = 1
|
||||
alertDialog.setView(input)
|
||||
alertDialog.setPositiveButton("OK") { dialog, which ->
|
||||
alertDialog.setPositiveButton("OK") { _, _ ->
|
||||
downloadNChaptersFrom(bindingAdapterPosition, input.value)
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
@@ -249,7 +262,7 @@ class MangaChapterAdapter(
|
||||
when (holder) {
|
||||
is ChapterCompactViewHolder -> {
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
val ep = arr[position]
|
||||
val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
|
||||
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
|
||||
@@ -275,7 +288,7 @@ class MangaChapterAdapter(
|
||||
val binding = holder.binding
|
||||
val ep = arr[position]
|
||||
holder.bind(ep.number, ep.progress)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
binding.itemChapterNumber.text = ep.number
|
||||
if (ep.progress.isNullOrEmpty()) {
|
||||
binding.itemChapterTitle.visibility = View.GONE
|
||||
|
||||
@@ -5,16 +5,25 @@ import java.util.regex.Pattern
|
||||
|
||||
class MangaNameAdapter {
|
||||
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? {
|
||||
val regex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)"
|
||||
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
|
||||
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
|
||||
val matcher: Matcher = pattern.matcher(text)
|
||||
|
||||
return if (matcher.find()) {
|
||||
matcher.group(2)?.toFloat()
|
||||
} 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,34 +2,42 @@ package ani.dantotsu.media.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.NumberPicker
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.media.anime.handleProgress
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.others.webview.CookieCatcher
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.MangaReadSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class MangaReadAdapter(
|
||||
private val media: Media,
|
||||
private val fragment: MangaReadFragment,
|
||||
@@ -47,6 +55,8 @@ class MangaReadAdapter(
|
||||
return ViewHolder(bind)
|
||||
}
|
||||
|
||||
private var nestedDialog: AlertDialog? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
@@ -60,7 +70,14 @@ class MangaReadAdapter(
|
||||
null
|
||||
)
|
||||
}
|
||||
val offline =
|
||||
if (!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
||||
) View.GONE else View.VISIBLE
|
||||
|
||||
binding.animeSourceNameContainer.visibility = offline
|
||||
binding.animeSourceSettings.visibility = offline
|
||||
binding.animeSourceSearch.visibility = offline
|
||||
binding.animeSourceTitle.visibility = offline
|
||||
//Source Selection
|
||||
var source =
|
||||
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
@@ -117,7 +134,7 @@ class MangaReadAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
//Subscription
|
||||
//Grids
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
binding.animeSourceSubscribe,
|
||||
@@ -136,98 +153,157 @@ class MangaReadAdapter(
|
||||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||
}
|
||||
|
||||
//Icons
|
||||
binding.animeSourceGrid.visibility = View.GONE
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
binding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeNestedButton.setOnClickListener {
|
||||
|
||||
binding.animeScanlatorTop.setOnClickListener {
|
||||
val dialogView =
|
||||
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||
|
||||
// Dynamically add checkboxes
|
||||
|
||||
options.forEach { option ->
|
||||
val checkBox = CheckBox(currContext()).apply {
|
||||
text = option
|
||||
}
|
||||
//set checked if it's already selected
|
||||
if (media.selected!!.scanlators != null) {
|
||||
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
} else {
|
||||
checkBox.isChecked = true
|
||||
}
|
||||
checkboxContainer.addView(checkBox)
|
||||
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||
var refresh = false
|
||||
var run = false
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style =
|
||||
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.MangaDefaultView)
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
dialogBinding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
run = true
|
||||
}
|
||||
|
||||
// Create AlertDialog
|
||||
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { dialog, which ->
|
||||
//add unchecked to hidden
|
||||
hiddenScanlators.clear()
|
||||
for (i in 0 until checkboxContainer.childCount) {
|
||||
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||
if (!checkBox.isChecked) {
|
||||
hiddenScanlators.add(checkBox.text.toString())
|
||||
}
|
||||
}
|
||||
fragment.onScanlatorChange(hiddenScanlators)
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
binding.animeDownloadTop.setOnClickListener {
|
||||
//Alert dialog asking for the number of chapters to download
|
||||
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
alertDialog.setTitle("Multi Chapter Downloader")
|
||||
alertDialog.setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
input.minValue = 1
|
||||
input.maxValue = 20
|
||||
input.value = 1
|
||||
alertDialog.setView(input)
|
||||
alertDialog.setPositiveButton("OK") { dialog, which ->
|
||||
fragment.multiDownload(input.value)
|
||||
//Grids
|
||||
dialogBinding.animeSourceGrid.visibility = View.GONE
|
||||
var selected = when (style) {
|
||||
0 -> dialogBinding.animeSourceList
|
||||
1 -> dialogBinding.animeSourceCompact
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> dialogBinding.layoutText.text = "List"
|
||||
1 -> dialogBinding.layoutText.text = "Compact"
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
val dialog = alertDialog.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
var selected = when (style) {
|
||||
0 -> binding.animeSourceList
|
||||
1 -> binding.animeSourceCompact
|
||||
else -> binding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
fun selected(it: ImageView) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
binding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
fun selected(it: ImageButton) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
dialogBinding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 0
|
||||
dialogBinding.layoutText.text = "List"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 1
|
||||
dialogBinding.layoutText.text = "Compact"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||
toast("WebView not installed")
|
||||
}
|
||||
//start CookieCatcher activity
|
||||
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
|
||||
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
|
||||
val url = sourceHttp?.baseUrl
|
||||
url?.let {
|
||||
refresh = true
|
||||
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
ContextCompat.startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Multi download
|
||||
dialogBinding.downloadNo.text = "0"
|
||||
dialogBinding.animeDownloadTop.setOnClickListener {
|
||||
//Alert dialog asking for the number of chapters to download
|
||||
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
alertDialog.setTitle("Multi Chapter Downloader")
|
||||
alertDialog.setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
input.minValue = 1
|
||||
input.maxValue = 20
|
||||
input.value = 1
|
||||
alertDialog.setView(input)
|
||||
alertDialog.setPositiveButton("OK") { _, _ ->
|
||||
dialogBinding.downloadNo.text = "${input.value}"
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
val dialog = alertDialog.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
//Scanlator
|
||||
dialogBinding.animeScanlatorContainer.visibility =
|
||||
if (options.count() > 1) View.VISIBLE else View.GONE
|
||||
dialogBinding.scanlatorNo.text = "${options.count()}"
|
||||
dialogBinding.animeScanlatorTop.setOnClickListener {
|
||||
val dialogView2 =
|
||||
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer =
|
||||
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||
|
||||
// Dynamically add checkboxes
|
||||
options.forEach { option ->
|
||||
val checkBox = CheckBox(currContext()).apply {
|
||||
text = option
|
||||
}
|
||||
//set checked if it's already selected
|
||||
if (media.selected!!.scanlators != null) {
|
||||
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
} else {
|
||||
checkBox.isChecked = true
|
||||
}
|
||||
checkboxContainer.addView(checkBox)
|
||||
}
|
||||
|
||||
// Create AlertDialog
|
||||
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
.setView(dialogView2)
|
||||
.setPositiveButton("OK") { _, _ ->
|
||||
//add unchecked to hidden
|
||||
hiddenScanlators.clear()
|
||||
for (i in 0 until checkboxContainer.childCount) {
|
||||
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||
if (!checkBox.isChecked) {
|
||||
hiddenScanlators.add(checkBox.text.toString())
|
||||
}
|
||||
}
|
||||
fragment.onScanlatorChange(hiddenScanlators)
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
|
||||
.setTitle("Options")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { _, _ ->
|
||||
if (run) fragment.onIconPressed(style, reversed)
|
||||
if (dialogBinding.downloadNo.text != "0") {
|
||||
fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
|
||||
}
|
||||
if (refresh) fragment.loadChapters(source, true)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ ->
|
||||
if (refresh) fragment.loadChapters(source, true)
|
||||
}
|
||||
.setOnCancelListener {
|
||||
if (refresh) fragment.loadChapters(source, true)
|
||||
}
|
||||
.create()
|
||||
nestedDialog?.show()
|
||||
}
|
||||
//Chapter Handling
|
||||
handleChapters()
|
||||
}
|
||||
@@ -259,6 +335,7 @@ class MangaReadAdapter(
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
|
||||
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
|
||||
val startChapterString = if (startChapter != null) {
|
||||
@@ -314,7 +391,8 @@ class MangaReadAdapter(
|
||||
if (media.manga?.chapters != null) {
|
||||
val chapters = media.manga.chapters!!.keys.toTypedArray()
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
|
||||
val appEp = PrefManager.getCustomVal<String?>("${media.id}_current_chp", null)
|
||||
?.toIntOrNull() ?: 1
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
val filteredChapters = chapters.filter { chapterKey ->
|
||||
val chapter = media.manga.chapters!![chapterKey]!!
|
||||
@@ -370,7 +448,7 @@ class MangaReadAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguageList(lang: Int, source: Int) {
|
||||
private fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (mangaReadSources is MangaSources) {
|
||||
val parser = mangaReadSources[source] as? DynamicMangaParser
|
||||
@@ -385,12 +463,16 @@ class MangaReadAdapter(
|
||||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
binding?.animeSourceLanguage?.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { it.lang })
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,11 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||
@@ -37,12 +38,14 @@ import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaParser
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.subcriptions.Notifications
|
||||
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
@@ -85,9 +88,6 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
var continueEp: Boolean = false
|
||||
var loaded = false
|
||||
|
||||
val uiSettings = loadData("ui_settings", toast = false)
|
||||
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -138,6 +138,23 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
|
||||
binding.animeSourceRecycler.layoutManager = gridLayoutManager
|
||||
|
||||
binding.ScrollTop.setOnClickListener {
|
||||
binding.animeSourceRecycler.scrollToPosition(10)
|
||||
binding.animeSourceRecycler.smoothScrollToPosition(0)
|
||||
}
|
||||
binding.animeSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val position = gridLayoutManager.findFirstVisibleItemPosition()
|
||||
if (position > 2) {
|
||||
binding.ScrollTop.translationY = -navBarHeight.toFloat()
|
||||
binding.ScrollTop.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.ScrollTop.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
})
|
||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||
if (it) binding.animeSourceRecycler.scrollToPosition(0)
|
||||
}
|
||||
@@ -153,7 +170,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
media.selected = model.loadSelected(media)
|
||||
|
||||
subscribed =
|
||||
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
||||
SubscriptionHelper.getSubscriptions().containsKey(media.id)
|
||||
|
||||
style = media.selected!!.recyclerStyle
|
||||
reverse = media.selected!!.recyclerReversed
|
||||
@@ -164,10 +181,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
|
||||
headerAdapter.scanlatorSelectionListener = this
|
||||
chapterAdapter =
|
||||
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
||||
MangaChapterAdapter(
|
||||
style ?: PrefManager.getVal(PrefName.MangaDefaultView), media, this
|
||||
)
|
||||
|
||||
for (download in downloadManager.mangaDownloads) {
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
for (download in downloadManager.mangaDownloadedTypes) {
|
||||
if (download.title == media.mainName()) {
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
}
|
||||
|
||||
binding.animeSourceRecycler.adapter =
|
||||
@@ -202,19 +223,25 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
val selected = media.userProgress
|
||||
val chapters = media.manga?.chapters?.values?.toList()
|
||||
//filter by selected language
|
||||
val progressChapterIndex = chapters?.indexOfFirst { MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected }?:0
|
||||
val chaptersToDownload = chapters?.subList(
|
||||
progressChapterIndex + 1,
|
||||
progressChapterIndex + n + 1
|
||||
)
|
||||
if (chaptersToDownload != null) {
|
||||
for (chapter in chaptersToDownload) {
|
||||
onMangaChapterDownloadClick(chapter.title!!)
|
||||
}
|
||||
}
|
||||
val progressChapterIndex = (chapters?.indexOfFirst {
|
||||
MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||
} ?: 0) + 1
|
||||
|
||||
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
|
||||
|
||||
// Calculate the end index
|
||||
val endIndex = minOf(progressChapterIndex + n, chapters.size)
|
||||
|
||||
//make sure there are enough chapters
|
||||
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
|
||||
|
||||
|
||||
for (chapter in chaptersToDownload) {
|
||||
onMangaChapterDownloadClick(chapter.title!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateChapters() {
|
||||
val loadedChapters = model.getMangaChapters().value
|
||||
if (loadedChapters != null) {
|
||||
@@ -259,7 +286,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
}
|
||||
|
||||
fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
|
||||
private fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
|
||||
val scanlators = mutableListOf<String>()
|
||||
if (chap != null) {
|
||||
val chapters = chap.values
|
||||
@@ -277,7 +304,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
model.mangaReadSources?.get(selected.sourceIndex)?.showUserTextListener = null
|
||||
selected.sourceIndex = i
|
||||
selected.server = null
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
media.selected = selected
|
||||
return model.mangaReadSources?.get(i)!!
|
||||
}
|
||||
@@ -285,14 +312,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
fun onLangChange(i: Int) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.langIndex = i
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
fun onScanlatorChange(list: List<String>) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.scanlators = list
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
@@ -305,7 +332,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
reverse = rev
|
||||
media.selected!!.recyclerStyle = style
|
||||
media.selected!!.recyclerReversed = reverse
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
model.saveSelected(media.id, media.selected!!)
|
||||
reload()
|
||||
}
|
||||
|
||||
@@ -313,7 +340,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
media.selected!!.chip = i
|
||||
start = s
|
||||
end = e
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
model.saveSelected(media.id, media.selected!!)
|
||||
reload()
|
||||
}
|
||||
|
||||
@@ -338,33 +365,34 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
|
||||
fun openSettings(pkg: MangaExtension.Installed) {
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as MediaDetailsActivity
|
||||
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
|
||||
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
|
||||
}
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
var itemSelected = false
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
val names =
|
||||
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
.setSingleChoiceItems(names, -1) { dialog, which ->
|
||||
selectedSetting = allSettings[which]
|
||||
itemSelected = true
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
@@ -379,10 +407,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
changeUIVisibility(true)
|
||||
return@setNegativeButton
|
||||
.setOnDismissListener {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
@@ -410,7 +438,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let {
|
||||
media.manga?.selectedChapter = i
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
model.saveSelected(media.id, media.selected!!)
|
||||
ChapterLoaderDialog.newInstance(it, true)
|
||||
.show(requireActivity().supportFragmentManager, "dialog")
|
||||
}
|
||||
@@ -437,7 +465,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
|
||||
// Create a download task
|
||||
val downloadTask = MangaDownloaderService.DownloadTask(
|
||||
title = media.nameMAL ?: media.nameRomaji,
|
||||
title = media.mainName(),
|
||||
chapter = chapter.title!!,
|
||||
imageData = images,
|
||||
sourceMedia = media,
|
||||
@@ -478,10 +506,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
|
||||
fun onMangaChapterRemoveDownloadClick(i: String) {
|
||||
downloadManager.removeDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
Download.Type.MANGA
|
||||
DownloadedType.Type.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.deleteDownload(i)
|
||||
@@ -496,10 +524,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
|
||||
// Remove the download from the manager and update the UI
|
||||
downloadManager.removeDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
Download.Type.MANGA
|
||||
DownloadedType.Type.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.purgeDownload(i)
|
||||
@@ -548,7 +576,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
selected.latest =
|
||||
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
headerAdapter.handleChapters()
|
||||
chapterAdapter.notifyItemRangeRemoved(0, chapterAdapter.arr.size)
|
||||
var arr: ArrayList<MangaChapter> = arrayListOf()
|
||||
@@ -562,7 +590,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr
|
||||
}
|
||||
chapterAdapter.arr = arr
|
||||
chapterAdapter.updateType(style ?: uiSettings.mangaDefaultView)
|
||||
chapterAdapter.updateType(style ?: PrefManager.getVal(PrefName.MangaDefaultView))
|
||||
chapterAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,7 @@ abstract class BaseImageAdapter(
|
||||
val activity: MangaReaderActivity,
|
||||
chapter: MangaChapter
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
val settings = activity.settings.default
|
||||
val uiSettings = activity.uiSettings
|
||||
val settings = activity.defaultSettings
|
||||
val images = chapter.images()
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -89,7 +88,8 @@ abstract class BaseImageAdapter(
|
||||
}
|
||||
} else {
|
||||
val detector = GestureDetectorCompat(view.context, object : GesturesListener() {
|
||||
override fun onSingleClick(event: MotionEvent) = activity.handleController()
|
||||
override fun onSingleClick(event: MotionEvent) =
|
||||
activity.handleController(event = event)
|
||||
})
|
||||
view.findViewById<View>(R.id.imgProgCover).apply {
|
||||
setOnTouchListener { _, event ->
|
||||
@@ -112,6 +112,9 @@ abstract class BaseImageAdapter(
|
||||
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) }
|
||||
}
|
||||
|
||||
abstract fun isZoomed(): Boolean
|
||||
abstract fun setZoom(zoom: Float)
|
||||
|
||||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -50,7 +50,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
||||
if (model.loadMangaChapterImages(
|
||||
chp,
|
||||
m.selected!!,
|
||||
m.nameMAL ?: m.nameRomaji
|
||||
m.mainName()
|
||||
)
|
||||
) {
|
||||
val activity = currActivity()
|
||||
|
||||
@@ -14,6 +14,8 @@ import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.LEFT_TO_RIGHT
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.RIGHT_TO_LEFT
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.PAGED
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
@@ -83,7 +85,7 @@ open class ImageAdapter(
|
||||
imageView.minScale = scale
|
||||
|
||||
ObjectAnimator.ofFloat(parent, "alpha", 0f, 1f)
|
||||
.setDuration((400 * uiSettings.animationSpeed).toLong())
|
||||
.setDuration((400 * PrefManager.getVal<Float>(PrefName.AnimationSpeed)).toLong())
|
||||
.start()
|
||||
progress.visibility = View.GONE
|
||||
|
||||
@@ -91,4 +93,16 @@ open class ImageAdapter(
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = images.size
|
||||
|
||||
override fun isZoomed(): Boolean {
|
||||
val imageView =
|
||||
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
|
||||
return imageView.scale > imageView.minScale
|
||||
}
|
||||
|
||||
override fun setZoom(zoom: Float) {
|
||||
val imageView =
|
||||
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
|
||||
imageView.setScaleAndCenter(zoom, imageView.center)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package ani.dantotsu.media.manga.mangareader
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -26,6 +28,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.connections.discord.DiscordService
|
||||
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
||||
@@ -39,27 +42,29 @@ import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaImage
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Companion.applyWebtoon
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.*
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.*
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
|
||||
import ani.dantotsu.settings.ReaderSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
import kotlin.properties.Delegates
|
||||
@@ -72,6 +77,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
private val model: MediaDetailsViewModel by viewModels()
|
||||
private val scope = lifecycleScope
|
||||
|
||||
var defaultSettings = CurrentReaderSettings()
|
||||
|
||||
private lateinit var media: Media
|
||||
private lateinit var chapter: MangaChapter
|
||||
private lateinit var chapters: MutableMap<String, MangaChapter>
|
||||
@@ -81,13 +88,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
private var isContVisible = false
|
||||
private var showProgressDialog = true
|
||||
private var progressDialog: AlertDialog.Builder? = null
|
||||
|
||||
private var maxChapterPage = 0L
|
||||
private var currentChapterPage = 0L
|
||||
|
||||
lateinit var settings: ReaderSettings
|
||||
lateinit var uiSettings: UserInterfaceSettings
|
||||
|
||||
private var notchHeight: Int? = null
|
||||
|
||||
private var imageAdapter: BaseImageAdapter? = null
|
||||
@@ -95,10 +99,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
var sliding = false
|
||||
var isAnimating = false
|
||||
|
||||
private var rpc: RPC? = null
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
|
||||
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
@@ -120,7 +122,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun hideBars() {
|
||||
if (!settings.showSystemBars) hideSystemBars()
|
||||
if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) hideSystemBars()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -135,28 +137,20 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.mangaReaderBack.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
progress { finish() }
|
||||
}
|
||||
|
||||
settings = loadData("reader_settings", this)
|
||||
?: ReaderSettings().apply { saveData("reader_settings", this) }
|
||||
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply {
|
||||
saveData(
|
||||
"ui_settings",
|
||||
this
|
||||
)
|
||||
}
|
||||
controllerDuration = (uiSettings.animationSpeed * 200).toLong()
|
||||
controllerDuration = (PrefManager.getVal<Float>(PrefName.AnimationSpeed) * 200).toLong()
|
||||
|
||||
hideBars()
|
||||
|
||||
@@ -179,7 +173,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.mangaReaderSlider.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
sliding = true
|
||||
if (settings.default.layout != PAGED)
|
||||
if (defaultSettings.layout != PAGED)
|
||||
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
|
||||
?: 1))
|
||||
else
|
||||
@@ -202,34 +196,30 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
else model.getMedia().value ?: return
|
||||
model.setMedia(media)
|
||||
|
||||
if (settings.autoDetectWebtoon && media.countryOfOrigin != "JP") applyWebtoon(settings.default)
|
||||
settings.default = loadData("${media.id}_current_settings") ?: settings.default
|
||||
if (PrefManager.getVal(PrefName.AutoDetectWebtoon) && media.countryOfOrigin != "JP") applyWebtoon(
|
||||
defaultSettings
|
||||
)
|
||||
defaultSettings = loadReaderSettings("${media.id}_current_settings") ?: defaultSettings
|
||||
|
||||
chapters = media.manga?.chapters ?: return
|
||||
chapter = chapters[media.manga!!.selectedChapter] ?: return
|
||||
|
||||
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
|
||||
binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE
|
||||
binding.mangaReaderSource.visibility =
|
||||
if (PrefManager.getVal(PrefName.ShowSource)) View.VISIBLE else View.GONE
|
||||
if (model.mangaReadSources!!.names.isEmpty()) {
|
||||
//try to reload sources
|
||||
try {
|
||||
if (media.isAdult) {
|
||||
val mangaSources = MangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
} else {
|
||||
val mangaSources = HMangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
val mangaSources = MangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(
|
||||
Injekt.get<MangaExtensionManager>().installedExtensionsFlow
|
||||
)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
} catch (e: Exception) {
|
||||
Firebase.crashlytics.recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
@@ -252,29 +242,18 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
showProgressDialog =
|
||||
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false
|
||||
progressDialog =
|
||||
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) {
|
||||
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
|
||||
val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox)
|
||||
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) progressDialog = null
|
||||
saveData("${media.id}_progressDialog", isChecked)
|
||||
showProgressDialog = isChecked
|
||||
}
|
||||
AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle(getString(R.string.title_update_progress))
|
||||
.setView(dialogView)
|
||||
.apply {
|
||||
setOnCancelListener { hideBars() }
|
||||
}
|
||||
} else null
|
||||
if (PrefManager.getVal(PrefName.AskIndividualReader)) PrefManager.getCustomVal(
|
||||
"${media.id}_progressDialog",
|
||||
true
|
||||
) else false
|
||||
|
||||
//Chapter Change
|
||||
fun change(index: Int) {
|
||||
mangaCache.clear()
|
||||
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
|
||||
PrefManager.setCustomVal(
|
||||
"${media.id}_${chaptersArr[currentChapterIndex]}",
|
||||
currentChapterPage
|
||||
)
|
||||
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!)
|
||||
.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
@@ -323,7 +302,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
chapter = chap
|
||||
media.manga!!.selectedChapter = chapter.number
|
||||
media.selected = model.loadSelected(media)
|
||||
saveData("${media.id}_current_chp", chap.number, this)
|
||||
PrefManager.setCustomVal("${media.id}_current_chp", chap.number)
|
||||
currentChapterIndex = chaptersArr.indexOf(chap.number)
|
||||
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
||||
binding.mangaReaderNextChap.text =
|
||||
@@ -332,9 +311,9 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
applySettings()
|
||||
val context = this
|
||||
val incognito = context.getSharedPreferences("Dantotsu", 0)
|
||||
?.getBoolean("incognito", false) ?: false
|
||||
if (isOnline(context) && Discord.token != null && !incognito) {
|
||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
|
||||
lifecycleScope.launch {
|
||||
val presence = RPC.createPresence(
|
||||
RPC.Companion.RPCData(
|
||||
@@ -374,7 +353,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
model.loadMangaChapterImages(
|
||||
chapter,
|
||||
media.selected!!,
|
||||
media.nameMAL ?: media.nameRomaji
|
||||
media.mainName()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -382,7 +361,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
|
||||
fun <T> dualPage(callback: () -> T): T? {
|
||||
return when (settings.default.dualPageMode) {
|
||||
return when (defaultSettings.dualPageMode) {
|
||||
No -> null
|
||||
Automatic -> {
|
||||
val orientation = resources.configuration.orientation
|
||||
@@ -397,29 +376,29 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun applySettings() {
|
||||
|
||||
saveData("${media.id}_current_settings", settings.default)
|
||||
saveReaderSettings("${media.id}_current_settings", defaultSettings)
|
||||
hideBars()
|
||||
|
||||
//true colors
|
||||
SubsamplingScaleImageView.setPreferredBitmapConfig(
|
||||
if (settings.default.trueColors) Bitmap.Config.ARGB_8888
|
||||
if (defaultSettings.trueColors) Bitmap.Config.ARGB_8888
|
||||
else Bitmap.Config.RGB_565
|
||||
)
|
||||
|
||||
//keep screen On
|
||||
if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
if (defaultSettings.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
binding.mangaReaderPager.unregisterOnPageChangeCallback(pageChangeCallback)
|
||||
|
||||
currentChapterPage = loadData("${media.id}_${chapter.number}", this) ?: 1
|
||||
currentChapterPage = PrefManager.getCustomVal("${media.id}_${chapter.number}", 1L)
|
||||
|
||||
val chapImages = chapter.images()
|
||||
|
||||
maxChapterPage = 0
|
||||
if (chapImages.isNotEmpty()) {
|
||||
maxChapterPage = chapImages.size.toLong()
|
||||
saveData("${media.id}_${chapter.number}_max", maxChapterPage)
|
||||
PrefManager.setCustomVal("${media.id}_${chapter.number}_max", maxChapterPage)
|
||||
|
||||
imageAdapter =
|
||||
dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter)
|
||||
@@ -434,15 +413,15 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.mangaReaderSlider.visibility = View.GONE
|
||||
}
|
||||
binding.mangaReaderPageNumber.text =
|
||||
if (settings.default.hidePageNumbers) "" else "${currentChapterPage}/$maxChapterPage"
|
||||
if (defaultSettings.hidePageNumbers) "" else "${currentChapterPage}/$maxChapterPage"
|
||||
|
||||
}
|
||||
|
||||
val currentPage = currentChapterPage.toInt()
|
||||
|
||||
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) {
|
||||
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP)) {
|
||||
binding.mangaReaderSwipy.vertical = true
|
||||
if (settings.default.direction == TOP_TO_BOTTOM) {
|
||||
if (defaultSettings.direction == TOP_TO_BOTTOM) {
|
||||
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
@@ -479,7 +458,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
} else {
|
||||
binding.mangaReaderSwipy.vertical = false
|
||||
if (settings.default.direction == RIGHT_TO_LEFT) {
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT) {
|
||||
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
@@ -516,11 +495,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.default.layout != PAGED) {
|
||||
if (defaultSettings.layout != PAGED) {
|
||||
|
||||
binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE
|
||||
binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled =
|
||||
settings.default.rotation
|
||||
defaultSettings.rotation
|
||||
|
||||
val detector = GestureDetectorCompat(this, object : GesturesListener() {
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
@@ -543,7 +522,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
val page =
|
||||
chapter.dualPages().getOrNull(pos) ?: return@dualPage false
|
||||
val nextPage = page.second
|
||||
if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null)
|
||||
if (defaultSettings.direction != LEFT_TO_RIGHT && nextPage != null)
|
||||
onImageLongClicked(pos * 2, nextPage, page.first, callback)
|
||||
else
|
||||
onImageLongClicked(pos * 2, page.first, nextPage, callback)
|
||||
@@ -565,11 +544,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
val manager = PreloadLinearLayoutManager(
|
||||
this,
|
||||
if (settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)
|
||||
if (defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP)
|
||||
RecyclerView.VERTICAL
|
||||
else
|
||||
RecyclerView.HORIZONTAL,
|
||||
!(settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == LEFT_TO_RIGHT)
|
||||
!(defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == LEFT_TO_RIGHT)
|
||||
)
|
||||
manager.preloadItemCount = 5
|
||||
|
||||
@@ -588,7 +567,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
|
||||
settings.default.apply {
|
||||
defaultSettings.apply {
|
||||
if (
|
||||
((direction == TOP_TO_BOTTOM || direction == BOTTOM_TO_TOP)
|
||||
&& (!v.canScrollVertically(-1) || !v.canScrollVertically(1)))
|
||||
@@ -607,25 +586,25 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
super.onScrolled(v, dx, dy)
|
||||
}
|
||||
})
|
||||
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP))
|
||||
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP))
|
||||
updatePadding(0, 128f.px, 0, 128f.px)
|
||||
else
|
||||
updatePadding(128f.px, 0, 128f.px, 0)
|
||||
|
||||
snapHelper.attachToRecyclerView(
|
||||
if (settings.default.layout == CONTINUOUS_PAGED) this
|
||||
if (defaultSettings.layout == CONTINUOUS_PAGED) this
|
||||
else null
|
||||
)
|
||||
|
||||
onVolumeUp = {
|
||||
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP))
|
||||
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP))
|
||||
smoothScrollBy(0, -500)
|
||||
else
|
||||
smoothScrollBy(-500, 0)
|
||||
}
|
||||
|
||||
onVolumeDown = {
|
||||
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP))
|
||||
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP))
|
||||
smoothScrollBy(0, 500)
|
||||
else
|
||||
smoothScrollBy(500, 0)
|
||||
@@ -640,11 +619,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
visibility = View.VISIBLE
|
||||
adapter = imageAdapter
|
||||
layoutDirection =
|
||||
if (settings.default.direction == BOTTOM_TO_TOP || settings.default.direction == RIGHT_TO_LEFT)
|
||||
if (defaultSettings.direction == BOTTOM_TO_TOP || defaultSettings.direction == RIGHT_TO_LEFT)
|
||||
View.LAYOUT_DIRECTION_RTL
|
||||
else View.LAYOUT_DIRECTION_LTR
|
||||
orientation =
|
||||
if (settings.default.direction == LEFT_TO_RIGHT || settings.default.direction == RIGHT_TO_LEFT)
|
||||
if (defaultSettings.direction == LEFT_TO_RIGHT || defaultSettings.direction == RIGHT_TO_LEFT)
|
||||
ViewPager2.ORIENTATION_HORIZONTAL
|
||||
else ViewPager2.ORIENTATION_VERTICAL
|
||||
registerOnPageChangeCallback(pageChangeCallback)
|
||||
@@ -667,7 +646,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
return when (event.keyCode) {
|
||||
KEYCODE_VOLUME_UP, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP -> {
|
||||
if (event.keyCode == KEYCODE_VOLUME_UP)
|
||||
if (!settings.default.volumeButtons)
|
||||
if (!defaultSettings.volumeButtons)
|
||||
return false
|
||||
if (event.action == ACTION_DOWN) {
|
||||
onVolumeUp?.invoke()
|
||||
@@ -677,7 +656,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> {
|
||||
if (event.keyCode == KEYCODE_VOLUME_DOWN)
|
||||
if (!settings.default.volumeButtons)
|
||||
if (!defaultSettings.volumeButtons)
|
||||
return false
|
||||
if (event.action == ACTION_DOWN) {
|
||||
onVolumeDown?.invoke()
|
||||
@@ -717,14 +696,66 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
goneTimer.schedule(timerTask, controllerDuration)
|
||||
}
|
||||
|
||||
fun handleController(shouldShow: Boolean? = null) {
|
||||
enum class pressPos {
|
||||
LEFT, RIGHT, CENTER
|
||||
}
|
||||
|
||||
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
|
||||
var pressLocation = pressPos.CENTER
|
||||
if (!sliding) {
|
||||
if (!settings.showSystemBars) {
|
||||
if (event != null && defaultSettings.layout == PAGED) {
|
||||
if (event.action != MotionEvent.ACTION_UP) return
|
||||
val x = event.rawX.toInt()
|
||||
val y = event.rawY.toInt()
|
||||
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
|
||||
if (screenWidth / 5 in x + 1..<y) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
|
||||
pressPos.RIGHT
|
||||
} else {
|
||||
pressPos.LEFT
|
||||
}
|
||||
}
|
||||
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
|
||||
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
|
||||
pressPos.LEFT
|
||||
} else {
|
||||
pressPos.RIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if pressLocation is left or right go to previous or next page (paged mode only)
|
||||
if (pressLocation == pressPos.LEFT) {
|
||||
|
||||
if (binding.mangaReaderPager.currentItem > 0) {
|
||||
//if the current images zoomed in, go back to normal before going to previous page
|
||||
if (imageAdapter?.isZoomed() == true) {
|
||||
imageAdapter?.setZoom(1f)
|
||||
}
|
||||
binding.mangaReaderPager.currentItem -= 1
|
||||
return
|
||||
}
|
||||
|
||||
} else if (pressLocation == pressPos.RIGHT) {
|
||||
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
|
||||
//if the current images zoomed in, go back to normal before going to next page
|
||||
if (imageAdapter?.isZoomed() == true) {
|
||||
imageAdapter?.setZoom(1f)
|
||||
}
|
||||
//if right to left, go to previous page
|
||||
binding.mangaReaderPager.currentItem += 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
|
||||
hideBars()
|
||||
checkNotch()
|
||||
}
|
||||
//horizontal scrollbar
|
||||
if (settings.default.horizontalScrollBar) {
|
||||
if (defaultSettings.horizontalScrollBar) {
|
||||
binding.mangaReaderSliderContainer.updateLayoutParams {
|
||||
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
width = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
@@ -751,7 +782,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
binding.mangaReaderSlider.layoutDirection =
|
||||
if (settings.default.direction == RIGHT_TO_LEFT || settings.default.direction == BOTTOM_TO_TOP)
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP)
|
||||
View.LAYOUT_DIRECTION_RTL
|
||||
else View.LAYOUT_DIRECTION_LTR
|
||||
shouldShow?.apply { isContVisible = !this }
|
||||
@@ -789,9 +820,9 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
fun updatePageNumber(page: Long) {
|
||||
if (currentChapterPage != page) {
|
||||
currentChapterPage = page
|
||||
saveData("${media.id}_${chapter.number}", page, this)
|
||||
PrefManager.setCustomVal("${media.id}_${chapter.number}", page)
|
||||
binding.mangaReaderPageNumber.text =
|
||||
if (settings.default.hidePageNumbers) "" else "${currentChapterPage}/$maxChapterPage"
|
||||
if (defaultSettings.hidePageNumbers) "" else "${currentChapterPage}/$maxChapterPage"
|
||||
if (!sliding) binding.mangaReaderSlider.apply {
|
||||
value = clamp(currentChapterPage.toFloat(), 1f, valueTo)
|
||||
}
|
||||
@@ -802,7 +833,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
model.loadMangaChapterImages(
|
||||
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
||||
media.selected!!,
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
media.mainName(),
|
||||
false
|
||||
)
|
||||
loading = false
|
||||
@@ -811,10 +842,28 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
private fun progress(runnable: Runnable) {
|
||||
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
|
||||
if (showProgressDialog && progressDialog != null) {
|
||||
progressDialog?.setCancelable(false)
|
||||
?.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
||||
saveData("${media.id}_save_progress", true)
|
||||
showProgressDialog =
|
||||
if (PrefManager.getVal(PrefName.AskIndividualReader)) PrefManager.getCustomVal(
|
||||
"${media.id}_progressDialog",
|
||||
true
|
||||
)
|
||||
else false
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if (showProgressDialog && !incognito) {
|
||||
|
||||
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
|
||||
val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox)
|
||||
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
PrefManager.setCustomVal("${media.id}_progressDialog", !isChecked)
|
||||
showProgressDialog = !isChecked
|
||||
}
|
||||
AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle(getString(R.string.title_update_progress))
|
||||
.setView(dialogView)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
||||
PrefManager.setCustomVal("${media.id}_save_progress", true)
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
@@ -823,14 +872,20 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
dialog.dismiss()
|
||||
runnable.run()
|
||||
}
|
||||
?.setNegativeButton(getString(R.string.no)) { dialog, _ ->
|
||||
saveData("${media.id}_save_progress", false)
|
||||
.setNegativeButton(getString(R.string.no)) { dialog, _ ->
|
||||
PrefManager.setCustomVal("${media.id}_save_progress", false)
|
||||
dialog.dismiss()
|
||||
runnable.run()
|
||||
}
|
||||
progressDialog?.show()
|
||||
.setOnCancelListener { hideBars() }
|
||||
.create()
|
||||
.show()
|
||||
} else {
|
||||
if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true)
|
||||
if (!incognito && PrefManager.getCustomVal(
|
||||
"${media.id}_save_progress",
|
||||
true
|
||||
) && if (media.isAdult) PrefManager.getVal<Boolean>(PrefName.UpdateForHReader) else true
|
||||
)
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
@@ -843,6 +898,51 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> loadReaderSettings(
|
||||
fileName: String,
|
||||
context: Context? = null,
|
||||
toast: Boolean = true
|
||||
): T? {
|
||||
val a = context ?: currContext()
|
||||
try {
|
||||
if (a?.fileList() != null)
|
||||
if (fileName in a.fileList()) {
|
||||
val fileIS: FileInputStream = a.openFileInput(fileName)
|
||||
val objIS = ObjectInputStream(fileIS)
|
||||
val data = objIS.readObject() as T
|
||||
objIS.close()
|
||||
fileIS.close()
|
||||
return data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
|
||||
//try to delete the file
|
||||
try {
|
||||
a?.deleteFile(fileName)
|
||||
} catch (e: Exception) {
|
||||
Injekt.get<CrashlyticsInterface>().log("Failed to delete file $fileName")
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun saveReaderSettings(fileName: String, data: Any?, context: Context? = null) {
|
||||
tryWith {
|
||||
val a = context ?: currContext()
|
||||
if (a != null) {
|
||||
val fos: FileOutputStream = a.openFileOutput(fileName, Context.MODE_PRIVATE)
|
||||
val os = ObjectOutputStream(fos)
|
||||
os.writeObject(data)
|
||||
os.close()
|
||||
fos.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTransformation(mangaImage: MangaImage): BitmapTransformation? {
|
||||
return model.loadTransformation(mangaImage, media.selected!!.sourceIndex)
|
||||
}
|
||||
@@ -853,7 +953,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
img2: MangaImage?,
|
||||
callback: ((ImageViewDialog) -> Unit)? = null
|
||||
): Boolean {
|
||||
if (!settings.default.longClickImage) return false
|
||||
if (!defaultSettings.longClickImage) return false
|
||||
val title = "(Page ${pos + 1}${if (img2 != null) "-${pos + 2}" else ""}) ${
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex)?.replace(" : ", " - ") ?: ""
|
||||
} [${media.userPreferredName}]"
|
||||
@@ -867,8 +967,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
val parserTransformation2 = getTransformation(img2)
|
||||
if (parserTransformation2 != null) transforms2.add(parserTransformation2)
|
||||
}
|
||||
val threshold = settings.default.cropBorderThreshold
|
||||
if (settings.default.cropBorders) {
|
||||
val threshold = defaultSettings.cropBorderThreshold
|
||||
if (defaultSettings.cropBorders) {
|
||||
transforms1.add(RemoveBordersTransformation(true, threshold))
|
||||
transforms1.add(RemoveBordersTransformation(false, threshold))
|
||||
if (img2 != null) {
|
||||
|
||||
@@ -26,7 +26,7 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val activity = requireActivity() as MangaReaderActivity
|
||||
val settings = activity.settings.default
|
||||
val settings = activity.defaultSettings
|
||||
|
||||
binding.readerDirectionText.text =
|
||||
resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
|
||||
|
||||
@@ -22,18 +22,15 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.novel.NovelDownloaderService
|
||||
import ani.dantotsu.download.novel.NovelServiceDataSingleton
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -61,13 +58,10 @@ class NovelReadFragment : Fragment(),
|
||||
private var continueEp: Boolean = false
|
||||
var loaded = false
|
||||
|
||||
val uiSettings = loadData("ui_settings", toast = false)
|
||||
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
||||
|
||||
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
|
||||
Log.e("downloadTrigger", novelDownloadPackage.link)
|
||||
val downloadTask = NovelDownloaderService.DownloadTask(
|
||||
title = media.nameMAL ?: media.nameRomaji,
|
||||
title = media.mainName(),
|
||||
chapter = novelDownloadPackage.novelName,
|
||||
downloadLink = novelDownloadPackage.link,
|
||||
originalLink = novelDownloadPackage.originalLink,
|
||||
@@ -92,16 +86,16 @@ class NovelReadFragment : Fragment(),
|
||||
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
if (downloadsManager.queryDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
Download.Type.NOVEL
|
||||
DownloadedType.Type.NOVEL
|
||||
)
|
||||
)
|
||||
) {
|
||||
val file = File(
|
||||
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.novelLocation}/${media.nameMAL ?: media.nameRomaji}/${novel.name}/0.epub"
|
||||
"${DownloadsManager.novelLocation}/${media.mainName()}/${novel.name}/0.epub"
|
||||
)
|
||||
if (!file.exists()) return false
|
||||
val fileUri = FileProvider.getUriForFile(
|
||||
@@ -124,10 +118,10 @@ class NovelReadFragment : Fragment(),
|
||||
override fun downloadedCheck(novel: ShowResponse): Boolean {
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
return downloadsManager.queryDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
Download.Type.NOVEL
|
||||
DownloadedType.Type.NOVEL
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -135,10 +129,10 @@ class NovelReadFragment : Fragment(),
|
||||
override fun deleteDownload(novel: ShowResponse) {
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.removeDownload(
|
||||
Download(
|
||||
media.nameMAL ?: media.nameRomaji,
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
Download.Type.NOVEL
|
||||
DownloadedType.Type.NOVEL
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -247,14 +241,13 @@ class NovelReadFragment : Fragment(),
|
||||
headerAdapter.progress?.visibility = View.VISIBLE
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (auto || query == "") model.autoSearchNovels(media)
|
||||
//else model.searchNovels(query, source)
|
||||
else model.autoSearchNovels(media) //testing
|
||||
else model.searchNovels(query, source)
|
||||
}
|
||||
searching = true
|
||||
if (save) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.server = query
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,7 +257,7 @@ class NovelReadFragment : Fragment(),
|
||||
selected.sourceIndex = i
|
||||
source = i
|
||||
selected.server = null
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
model.saveSelected(media.id, selected)
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
|
||||
@@ -35,14 +35,18 @@ class NovelResponseAdapter(
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val novel = list[position]
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
|
||||
val cover = GlideUrl(novel.coverUrl.url) { novel.coverUrl.headers }
|
||||
Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0)
|
||||
.into(binding.itemEpisodeImage)
|
||||
|
||||
val typedValue = TypedValue()
|
||||
fragment.requireContext().theme?.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue, true)
|
||||
fragment.requireContext().theme?.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
val color = typedValue.data
|
||||
|
||||
binding.itemEpisodeTitle.text = novel.name
|
||||
@@ -98,14 +102,19 @@ class NovelResponseAdapter(
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(fragment.requireContext(), R.style.DialogTheme)
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(
|
||||
fragment.requireContext(),
|
||||
R.style.MyPopup
|
||||
)
|
||||
builder.setTitle("Delete ${novel.name}?")
|
||||
builder.setMessage("Are you sure you want to delete ${novel.name}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadedCheckCallback.deleteDownload(novel)
|
||||
deleteDownload(novel.link)
|
||||
snackString("Deleted ${novel.name}")
|
||||
if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) {
|
||||
if (binding.itemEpisodeFiller.text.toString()
|
||||
.contains("Download", ignoreCase = true)
|
||||
) {
|
||||
binding.itemEpisodeFiller.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package ani.dantotsu.media.novel.novelreader
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Color
|
||||
@@ -15,6 +16,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.webkit.WebView
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
@@ -27,17 +29,16 @@ import androidx.webkit.WebViewCompat
|
||||
import ani.dantotsu.GesturesListener
|
||||
import ani.dantotsu.NoPaddingArrayAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ActivityNovelReaderBinding
|
||||
import ani.dantotsu.hideSystemBars
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.CurrentNovelReaderSettings
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
import ani.dantotsu.settings.NovelReaderSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.tryWith
|
||||
@@ -51,8 +52,13 @@ import com.vipulog.ebookreader.RelocationInfo
|
||||
import com.vipulog.ebookreader.TocItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
import kotlin.properties.Delegates
|
||||
@@ -62,9 +68,6 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
private lateinit var binding: ActivityNovelReaderBinding
|
||||
private val scope = lifecycleScope
|
||||
|
||||
lateinit var settings: NovelReaderSettings
|
||||
private lateinit var uiSettings: UserInterfaceSettings
|
||||
|
||||
private var notchHeight: Int? = null
|
||||
|
||||
var loaded = false
|
||||
@@ -77,6 +80,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
|
||||
val themes = ArrayList<ReaderTheme>()
|
||||
|
||||
var defaultSettings = CurrentNovelReaderSettings()
|
||||
|
||||
|
||||
init {
|
||||
val forestTheme = ReaderTheme(
|
||||
@@ -139,33 +144,43 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("WebViewApiAvailability")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//check for supported webview
|
||||
val webViewVersion = WebViewCompat.getCurrentWebViewPackage(this)?.versionName
|
||||
val webViewVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
WebView.getCurrentWebViewPackage()?.versionName
|
||||
} else {
|
||||
WebViewCompat.getCurrentWebViewPackage(this)?.versionName
|
||||
}
|
||||
val firstVersion = webViewVersion?.split(".")?.firstOrNull()?.toIntOrNull()
|
||||
if (webViewVersion == null || firstVersion == null || firstVersion < 87) {
|
||||
Toast.makeText(this, "Please update WebView from PlayStore", Toast.LENGTH_LONG).show()
|
||||
val text = if (webViewVersion == null) {
|
||||
"Could not find webView installed"
|
||||
} else if (firstVersion == null) {
|
||||
"Could not find WebView Version Number: $webViewVersion"
|
||||
} else if (firstVersion < 87) { //false positive?
|
||||
"Webview Versiom: $firstVersion. PLease update"
|
||||
} else {
|
||||
"Please update WebView from PlayStore"
|
||||
}
|
||||
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
|
||||
//open playstore
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview")
|
||||
intent.data =
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview")
|
||||
startActivity(intent)
|
||||
//stop reader
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
settings = loadData("novel_reader_settings", this)
|
||||
?: NovelReaderSettings().apply { saveData("novel_reader_settings", this) }
|
||||
uiSettings = loadData("ui_settings", this)
|
||||
?: UserInterfaceSettings().also { saveData("ui_settings", it) }
|
||||
|
||||
controllerDuration = (uiSettings.animationSpeed * 200).toLong()
|
||||
controllerDuration = (PrefManager.getVal<Float>(PrefName.AnimationSpeed) * 200).toLong()
|
||||
|
||||
setupViews()
|
||||
setupBackPressedHandler()
|
||||
@@ -271,11 +286,12 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
binding.bookReader.getAppearance {
|
||||
currentTheme = it
|
||||
themes.add(0, it)
|
||||
settings.default = loadData("${sanitizedBookId}_current_settings") ?: settings.default
|
||||
defaultSettings =
|
||||
loadReaderSettings("${sanitizedBookId}_current_settings") ?: defaultSettings
|
||||
applySettings()
|
||||
}
|
||||
|
||||
val cfi = loadData<String>("${sanitizedBookId}_progress")
|
||||
val cfi = PrefManager.getCustomVal("${sanitizedBookId}_progress", null as String?)
|
||||
|
||||
cfi?.let { binding.bookReader.goto(it) }
|
||||
binding.progress.visibility = View.GONE
|
||||
@@ -288,7 +304,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
binding.novelReaderSlider.value = info.fraction.toFloat()
|
||||
val pos = info.tocItem?.let { item -> toc.indexOfFirst { it == item } }
|
||||
if (pos != null) binding.novelReaderChapterSelect.setSelection(pos)
|
||||
saveData("${sanitizedBookId}_progress", info.cfi)
|
||||
PrefManager.setCustomVal("${sanitizedBookId}_progress", info.cfi)
|
||||
}
|
||||
|
||||
|
||||
@@ -323,7 +339,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP)
|
||||
if (!settings.default.volumeButtons)
|
||||
if (!defaultSettings.volumeButtons)
|
||||
return false
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
onVolumeUp?.invoke()
|
||||
@@ -333,7 +349,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
|
||||
if (!settings.default.volumeButtons)
|
||||
if (!defaultSettings.volumeButtons)
|
||||
return false
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
onVolumeDown?.invoke()
|
||||
@@ -349,13 +365,18 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
|
||||
|
||||
fun applySettings() {
|
||||
saveData("${sanitizedBookId}_current_settings", settings.default)
|
||||
saveReaderSettings("${sanitizedBookId}_current_settings", defaultSettings)
|
||||
hideBars()
|
||||
|
||||
if (defaultSettings.useOledTheme) {
|
||||
themes.forEach { theme ->
|
||||
theme.darkBg = Color.parseColor("#000000")
|
||||
}
|
||||
}
|
||||
currentTheme =
|
||||
themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) }
|
||||
themes.first { it.name.equals(defaultSettings.currentThemeName, ignoreCase = true) }
|
||||
|
||||
when (settings.default.layout) {
|
||||
when (defaultSettings.layout) {
|
||||
CurrentNovelReaderSettings.Layouts.PAGED -> {
|
||||
currentTheme?.flow = ReaderFlow.PAGINATED
|
||||
}
|
||||
@@ -366,22 +387,22 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
}
|
||||
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
when (settings.default.dualPageMode) {
|
||||
when (defaultSettings.dualPageMode) {
|
||||
CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1
|
||||
CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2
|
||||
CurrentReaderSettings.DualPageModes.Force -> requestedOrientation =
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
|
||||
currentTheme?.lineHeight = settings.default.lineHeight
|
||||
currentTheme?.gap = settings.default.margin
|
||||
currentTheme?.maxInlineSize = settings.default.maxInlineSize
|
||||
currentTheme?.maxBlockSize = settings.default.maxBlockSize
|
||||
currentTheme?.useDark = settings.default.useDarkTheme
|
||||
currentTheme?.lineHeight = defaultSettings.lineHeight
|
||||
currentTheme?.gap = defaultSettings.margin
|
||||
currentTheme?.maxInlineSize = defaultSettings.maxInlineSize
|
||||
currentTheme?.maxBlockSize = defaultSettings.maxBlockSize
|
||||
currentTheme?.useDark = defaultSettings.useDarkTheme
|
||||
|
||||
currentTheme?.let { binding.bookReader.setAppearance(it) }
|
||||
|
||||
if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
if (defaultSettings.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
@@ -411,7 +432,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
fun handleController(shouldShow: Boolean? = null) {
|
||||
if (!loaded) return
|
||||
|
||||
if (!settings.showSystemBars) {
|
||||
if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
|
||||
hideBars()
|
||||
applyNotchMargin()
|
||||
}
|
||||
@@ -444,7 +465,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
|
||||
|
||||
private fun checkNotch() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
|
||||
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
@@ -465,8 +486,53 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> loadReaderSettings(
|
||||
fileName: String,
|
||||
context: Context? = null,
|
||||
toast: Boolean = true
|
||||
): T? {
|
||||
val a = context ?: currContext()
|
||||
try {
|
||||
if (a?.fileList() != null)
|
||||
if (fileName in a.fileList()) {
|
||||
val fileIS: FileInputStream = a.openFileInput(fileName)
|
||||
val objIS = ObjectInputStream(fileIS)
|
||||
val data = objIS.readObject() as T
|
||||
objIS.close()
|
||||
fileIS.close()
|
||||
return data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
|
||||
//try to delete the file
|
||||
try {
|
||||
a?.deleteFile(fileName)
|
||||
} catch (e: Exception) {
|
||||
Injekt.get<CrashlyticsInterface>().log("Failed to delete file $fileName")
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun saveReaderSettings(fileName: String, data: Any?, context: Context? = null) {
|
||||
tryWith {
|
||||
val a = context ?: currContext()
|
||||
if (a != null) {
|
||||
val fos: FileOutputStream = a.openFileOutput(fileName, Context.MODE_PRIVATE)
|
||||
val os = ObjectOutputStream(fos)
|
||||
os.writeObject(data)
|
||||
os.close()
|
||||
fos.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideBars() {
|
||||
if (!settings.showSystemBars) hideSystemBars()
|
||||
if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
|
||||
hideSystemBars()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,7 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val activity = requireActivity() as NovelReaderActivity
|
||||
val settings = activity.settings.default
|
||||
|
||||
val settings = activity.defaultSettings
|
||||
val themeLabels = activity.themes.map { it.name }
|
||||
binding.themeSelect.adapter =
|
||||
NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels)
|
||||
@@ -49,7 +48,11 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
binding.useOledTheme.isChecked = settings.useOledTheme
|
||||
binding.useOledTheme.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.useOledTheme = isChecked
|
||||
activity.applySettings()
|
||||
}
|
||||
val layoutList = listOf(
|
||||
binding.paged,
|
||||
binding.continuous
|
||||
@@ -173,6 +176,20 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.maxBlockSize.setText(value.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
}
|
||||
binding.incrementMaxBlockSize.setOnClickListener {
|
||||
val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.maxBlockSize = value + 10
|
||||
binding.maxBlockSize.setText(settings.maxBlockSize.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.decrementMaxBlockSize.setOnClickListener {
|
||||
val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.maxBlockSize = value - 10
|
||||
binding.maxBlockSize.setText(settings.maxBlockSize.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.useDarkTheme.isChecked = settings.useDarkTheme
|
||||
|
||||
@@ -4,20 +4,23 @@ import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
@@ -33,7 +36,7 @@ class ListActivity : AppCompatActivity() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
|
||||
@@ -61,8 +64,7 @@ class ListActivity : AppCompatActivity() {
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) {
|
||||
if (!PrefManager.getVal<Boolean>(PrefName.ImmersiveMode)) {
|
||||
this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||
binding.root.fitsSystemWindows = true
|
||||
@@ -74,13 +76,16 @@ class ListActivity : AppCompatActivity() {
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
setContentView(binding.root)
|
||||
|
||||
val anime = intent.getBooleanExtra("anime", true)
|
||||
binding.listTitle.text =
|
||||
intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
|
||||
|
||||
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
this@ListActivity.selectedTabIdx = tab?.position ?: 0
|
||||
@@ -145,7 +150,10 @@ class ListActivity : AppCompatActivity() {
|
||||
R.id.release -> "release"
|
||||
else -> null
|
||||
}
|
||||
|
||||
PrefManager.setVal(
|
||||
if (anime) PrefName.AnimeListSortOrder else PrefName.MangaListSortOrder,
|
||||
sort ?: ""
|
||||
)
|
||||
binding.listProgressBar.visibility = View.VISIBLE
|
||||
binding.listViewPager.adapter = null
|
||||
scope.launch {
|
||||
|
||||
@@ -46,7 +46,7 @@ class ListFragment : Fragment() {
|
||||
binding.listRecyclerView.layoutManager =
|
||||
GridLayoutManager(
|
||||
requireContext(),
|
||||
if (grid!!) (screenWidth / 124f).toInt() else 1
|
||||
if (grid!!) (screenWidth / 120f).toInt() else 1
|
||||
)
|
||||
binding.listRecyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
|
||||
class ListViewModel : ViewModel() {
|
||||
var grid = MutableLiveData(loadData<Boolean>("listGrid") ?: true)
|
||||
var grid = MutableLiveData(PrefManager.getVal<Boolean>(PrefName.ListGrid))
|
||||
|
||||
private val lists = MutableLiveData<MutableMap<String, ArrayList<Media>>>()
|
||||
fun getLists(): LiveData<MutableMap<String, ArrayList<Media>>> = lists
|
||||
|
||||
@@ -6,13 +6,17 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentOfflineBinding
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.statusBarHeight
|
||||
|
||||
class OfflineFragment : Fragment() {
|
||||
private var offline = false
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -23,6 +27,10 @@ class OfflineFragment : Fragment() {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
offline = PrefManager.getVal(PrefName.OfflineMode)
|
||||
binding.noInternet.text =
|
||||
if (offline) "Offline Mode" else getString(R.string.no_internet)
|
||||
binding.refreshButton.visibility = if (offline) View.GONE else View.VISIBLE
|
||||
binding.refreshButton.setOnClickListener {
|
||||
if (isOnline(requireContext())) {
|
||||
startMainActivity(requireActivity())
|
||||
@@ -30,4 +38,9 @@ class OfflineFragment : Fragment() {
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
offline = PrefManager.getVal(PrefName.OfflineMode)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
DisabledReports.kt
|
||||
@@ -2,7 +2,6 @@ package ani.dantotsu.others
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -55,10 +54,6 @@ open class CustomBottomDialog : BottomSheetDialogFragment() {
|
||||
_binding = BottomSheetCustomBinding.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
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user