mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-27 22:11:01 +00:00
Compare commits
405 Commits
v1.0.0-bet
...
v3.0.0
| 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 | ||
|
|
8797af0cbc | ||
|
|
97a4cba680 | ||
|
|
acc5069c83 | ||
|
|
fab978dba4 | ||
|
|
ad1734d640 | ||
|
|
d687911c85 | ||
|
|
1d4257b1b3 | ||
|
|
55521ab9fc | ||
|
|
17e53a54af | ||
|
|
dc1edc9a42 | ||
|
|
b8782b0507 | ||
|
|
0d422a57e7 | ||
|
|
1bbc98d350 | ||
|
|
7ae6831628 | ||
|
|
65e89398d9 | ||
|
|
2b77b7578c | ||
|
|
e77ab2800a | ||
|
|
c1a0eeb361 | ||
|
|
393ab1e513 | ||
|
|
e26a6c647f | ||
|
|
ea83b722a6 | ||
|
|
34a3e9e5a3 | ||
|
|
7f92ac686d | ||
|
|
b6c79dae40 | ||
|
|
8c957007ab | ||
|
|
c728eae2ba | ||
|
|
3ded6ba87a | ||
|
|
111fb16266 | ||
|
|
121be4bc6f | ||
|
|
afa960c808 | ||
|
|
1df528c0dc | ||
|
|
f792296f78 | ||
|
|
d512929387 | ||
|
|
c7bc1ffe9e | ||
|
|
32f918450a | ||
|
|
f01377f0b1 | ||
|
|
c2a07278fc | ||
|
|
0a17bed243 | ||
|
|
c5ed8acfa3 | ||
|
|
e5f2bb6566 | ||
|
|
b4093b0c47 | ||
|
|
d0fd62abf2 | ||
|
|
4d0c3e5849 | ||
|
|
d131562f34 | ||
|
|
cf2d9ad654 | ||
|
|
af326c8258 | ||
|
|
ba351df331 | ||
|
|
d4c2df37ae | ||
|
|
79d1c44e63 | ||
|
|
38faedb4b5 | ||
|
|
39b0f28127 | ||
|
|
a1913ed968 | ||
|
|
f7917df907 | ||
|
|
84c58fbe6c | ||
|
|
75895d851f | ||
|
|
6d05a42168 | ||
|
|
594fa4daa9 |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [rebelonion]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
custom: ['https://www.buymeacoffee.com/rebelonion']
|
||||||
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
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
|
|
||||||
# Google Services (e.g. APIs or Firebase)
|
|
||||||
google-services.json
|
|
||||||
|
|
||||||
# Android Profiling
|
# Android Profiling
|
||||||
*.hprof
|
*.hprof
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
|
<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://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
|
||||||
|
<a href="https://www.codefactor.io/repository/github/rebelonion/dantotsu"><img src="https://www.codefactor.io/repository/github/rebelonion/dantotsu/badge?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge" alt="CodeFactor" /></a>
|
||||||
|
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/discord/358599430502481920.svg?style=for-the-badge&logo=discord&colorB=7289DA"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# **Dantotsu** 🌟
|
# **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!
|
> **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!
|
### 🚀 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>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## VISITORS
|
||||||
|
|
||||||
|
<img src="https://count.getloli.com/get/@:rebeloniondantotsu" alt=":rebeloniondantotsu" />
|
||||||
|
|
||||||
## LICENSE 📜
|
## LICENSE 📜
|
||||||
|
|
||||||
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
||||||
|
|||||||
4
app/.gitignore
vendored
4
app/.gitignore
vendored
@@ -1,4 +1,8 @@
|
|||||||
/build
|
/build
|
||||||
/debug
|
/debug
|
||||||
/debug/output-metadata.json
|
/debug/output-metadata.json
|
||||||
|
/alpha
|
||||||
|
/alpha/output-metadata.json
|
||||||
|
/google/*
|
||||||
|
/fdroid/*
|
||||||
/release
|
/release
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'com.google.gms.google-services'
|
|
||||||
id 'com.google.firebase.crashlytics'
|
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlinx-serialization'
|
id 'kotlinx-serialization'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def gitCommitHash = providers.exec {
|
def gitCommitHash = providers.exec {
|
||||||
@@ -20,23 +17,51 @@ android {
|
|||||||
applicationId "ani.dantotsu"
|
applicationId "ani.dantotsu"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||||
versionName "1.0.0-beta03"
|
versionName "2.2.0"
|
||||||
|
versionCode 220000000
|
||||||
signingConfig signingConfigs.debug
|
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 {
|
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 {
|
debug {
|
||||||
applicationIdSuffix ".beta"
|
applicationIdSuffix ".beta"
|
||||||
debuggable true
|
versionNameSuffix "-beta01"
|
||||||
|
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
|
||||||
|
debuggable false
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
|
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
|
||||||
debuggable false
|
debuggable false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
@@ -50,21 +75,27 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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
|
// Core
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.browser:browser:1.6.0'
|
implementation 'androidx.browser:browser:1.7.0'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'com.google.code.gson:gson:2.8.9'
|
implementation 'com.google.code.gson:gson:2.10'
|
||||||
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
|
||||||
implementation 'androidx.preference:preference:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
|
implementation 'androidx.webkit:webkit:1.10.0'
|
||||||
|
|
||||||
// Glide
|
// Glide
|
||||||
ext.glide_version = '4.16.0'
|
ext.glide_version = '4.16.0'
|
||||||
@@ -74,22 +105,20 @@ dependencies {
|
|||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
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.3.0'
|
|
||||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.4.3'
|
|
||||||
|
|
||||||
// Exoplayer
|
// Exoplayer
|
||||||
ext.exo_version = '1.1.1'
|
ext.exo_version = '1.2.1'
|
||||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||||
implementation "androidx.media3:media3-ui:$exo_version"
|
implementation "androidx.media3:media3-ui:$exo_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||||
implementation "androidx.media3:media3-session:$exo_version"
|
implementation "androidx.media3:media3-session:$exo_version"
|
||||||
|
//media3 casting
|
||||||
|
implementation "androidx.media3:media3-cast:$exo_version"
|
||||||
|
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.google.android.material:material:1.10.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||||
@@ -97,6 +126,7 @@ dependencies {
|
|||||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||||
|
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||||
|
|
||||||
// string matching
|
// string matching
|
||||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||||
@@ -108,16 +138,18 @@ dependencies {
|
|||||||
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
|
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
|
||||||
implementation 'com.squareup.logcat:logcat:0.1'
|
implementation 'com.squareup.logcat:logcat:0.1'
|
||||||
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
|
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor: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.11'
|
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||||
implementation 'com.squareup.okio:okio:3.3.0'
|
implementation 'com.squareup.okio:okio:3.7.0'
|
||||||
implementation 'ch.acra:acra-http:5.9.7'
|
implementation '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.jsoup:jsoup:1.15.4'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.5.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
|
||||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
|
implementation 'app.cash.quickjs:quickjs-android:0.9.2'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
376
app/src/debug/res/drawable/anim_splash.xml
Normal file
376
app/src/debug/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="#6901fd"
|
||||||
|
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="#4800e5"
|
||||||
|
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="#2000bd"
|
||||||
|
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="#1e00d1"
|
||||||
|
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="#2900da"
|
||||||
|
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="#1f1f30"
|
||||||
|
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="#1f1f30"
|
||||||
|
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="#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>
|
||||||
|
</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>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Dantotsu</string>
|
<string name="app_name">Dantotsu β</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -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.core.content.getSystemService
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -29,41 +30,49 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object AppUpdater {
|
object AppUpdater {
|
||||||
suspend fun check(activity: FragmentActivity, post:Boolean=false) {
|
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||||
if(post) snackString(currContext()?.getString(R.string.checking_for_update))
|
if (post) snackString(currContext()?.getString(R.string.checking_for_update))
|
||||||
val repo = activity.getString(R.string.repo)
|
val repo = activity.getString(R.string.repo)
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
val (md, version) = if(BuildConfig.DEBUG){
|
val (md, version) = if (BuildConfig.DEBUG) {
|
||||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||||
.parsed<JsonArray>().map {
|
.parsed<JsonArray>().map {
|
||||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
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()
|
it.timeStamp()
|
||||||
} ?: throw Exception("No Pre Release Found")
|
} ?: throw Exception("No Pre Release Found")
|
||||||
val v = r.tagName.substringAfter("v","")
|
val v = r.tagName.substringAfter("v", "")
|
||||||
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||||
}else{
|
} else {
|
||||||
val res =
|
val res =
|
||||||
client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
|
client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
|
||||||
res to res.substringAfter("# ").substringBefore("\n")
|
res to res.substringAfter("# ").substringBefore("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger("Git Version : $version")
|
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 {
|
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
|
||||||
CustomBottomDialog.newInstance().apply {
|
CustomBottomDialog.newInstance().apply {
|
||||||
setTitleText("${if (BuildConfig.DEBUG) "Beta " else ""}Update " + currContext()!!.getString(R.string.available))
|
setTitleText(
|
||||||
|
"${if (BuildConfig.DEBUG) "Beta " else ""}Update " + currContext()!!.getString(
|
||||||
|
R.string.available
|
||||||
|
)
|
||||||
|
)
|
||||||
addView(
|
addView(
|
||||||
TextView(activity).apply {
|
TextView(activity).apply {
|
||||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
val markWon = Markwon.builder(activity)
|
||||||
|
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||||
markWon.setMarkdown(this, md)
|
markWon.setMarkdown(this, md)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
setCheck(currContext()!!.getString(R.string.dont_show_again, version), false) { isChecked ->
|
setCheck(
|
||||||
|
currContext()!!.getString(R.string.dont_show_again, version),
|
||||||
|
false
|
||||||
|
) { isChecked ->
|
||||||
if (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)) {
|
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||||
@@ -71,11 +80,11 @@ object AppUpdater {
|
|||||||
try {
|
try {
|
||||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||||
.parsed<GithubResponse>().assets?.find {
|
.parsed<GithubResponse>().assets?.find {
|
||||||
it.browserDownloadURL.endsWith("apk")
|
it.browserDownloadURL.endsWith("apk")
|
||||||
}?.browserDownloadURL.apply {
|
}?.browserDownloadURL.apply {
|
||||||
if (this != null) activity.downloadUpdate(version, this)
|
if (this != null) activity.downloadUpdate(version, this)
|
||||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
@@ -88,25 +97,24 @@ object AppUpdater {
|
|||||||
show(activity.supportFragmentManager, "dialog")
|
show(activity.supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else{
|
else {
|
||||||
if(post) snackString(currContext()?.getString(R.string.no_update_found))
|
if (post) snackString(currContext()?.getString(R.string.no_update_found))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun compareVersion(version: String): Boolean {
|
private fun compareVersion(version: String): Boolean {
|
||||||
|
|
||||||
if(BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
return BuildConfig.VERSION_NAME != version
|
return BuildConfig.VERSION_NAME != version
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
fun toDouble(list: List<String>): Double {
|
fun toDouble(list: List<String>): Double {
|
||||||
return list.mapIndexed { i: Int, s: String ->
|
return list.mapIndexed { i: Int, s: String ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> s.toDouble() * 100
|
0 -> s.toDouble() * 100
|
||||||
1 -> s.toDouble() * 10
|
1 -> s.toDouble() * 10
|
||||||
2 -> s.toDouble()
|
2 -> s.toDouble()
|
||||||
else -> s.toDoubleOrNull()?: 0.0
|
else -> s.toDoubleOrNull() ?: 0.0
|
||||||
}
|
}
|
||||||
}.sum()
|
}.sum()
|
||||||
}
|
}
|
||||||
@@ -174,12 +182,12 @@ object AppUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openApk(context: Context, uri: Uri) {
|
private fun openApk(context: Context, uri: Uri) {
|
||||||
try {
|
try {
|
||||||
uri.path?.let {
|
uri.path?.let {
|
||||||
val contentUri = FileProvider.getUriForFile(
|
val contentUri = FileProvider.getUriForFile(
|
||||||
@@ -210,7 +218,7 @@ object AppUpdater {
|
|||||||
val tagName: String,
|
val tagName: String,
|
||||||
val prerelease: Boolean,
|
val prerelease: Boolean,
|
||||||
@SerialName("created_at")
|
@SerialName("created_at")
|
||||||
val createdAt : String,
|
val createdAt: String,
|
||||||
val body: String? = null,
|
val body: String? = null,
|
||||||
val assets: List<Asset>? = null
|
val assets: List<Asset>? = null
|
||||||
) {
|
) {
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
|
||||||
|
tools:ignore="LeanbackUsesWifi" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
@@ -17,23 +20,22 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
android:maxSdkVersion="32" />
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||||
<!-- For background jobs -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
|
||||||
|
|
||||||
<!-- For managing extensions -->
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
|
||||||
<!-- To view extension packages in API 30+ -->
|
<uses-permission
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<uses-permission
|
||||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
@@ -46,22 +48,37 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:banner="@mipmap/ic_banner_foreground"
|
||||||
|
android:icon="${icon_placeholder}"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="${icon_placeholder_round}"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Dantotsu"
|
android:theme="@style/Theme.Dantotsu"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="AllowBackup"
|
tools:ignore="AllowBackup">
|
||||||
android:banner="@drawable/ic_banner_foreground">
|
<receiver
|
||||||
|
android:name=".widgets.CurrentlyAiringWidget"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/currently_airing_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
<receiver android:name=".subcriptions.NotificationClickReceiver" />
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity"
|
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
android:exported="true" >
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="application/epub+zip" />
|
<data android:mimeType="application/epub+zip" />
|
||||||
@@ -69,13 +86,11 @@
|
|||||||
<data android:mimeType="application/vnd.amazon.ebook" />
|
<data android:mimeType="application/vnd.amazon.ebook" />
|
||||||
<data android:mimeType="application/fb2+zip" />
|
<data android:mimeType="application/fb2+zip" />
|
||||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||||
|
|
||||||
<data android:pathPattern=".*\\.epub" />
|
<data android:pathPattern=".*\\.epub" />
|
||||||
<data android:pathPattern=".*\\.mobi" />
|
<data android:pathPattern=".*\\.mobi" />
|
||||||
<data android:pathPattern=".*\\.kf8" />
|
<data android:pathPattern=".*\\.kf8" />
|
||||||
<data android:pathPattern=".*\\.fb2" />
|
<data android:pathPattern=".*\\.fb2" />
|
||||||
<data android:pathPattern=".*\\.cbz" />
|
<data android:pathPattern=".*\\.cbz" />
|
||||||
|
|
||||||
<data android:scheme="content" />
|
<data android:scheme="content" />
|
||||||
<data android:scheme="file" />
|
<data android:scheme="file" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -101,9 +116,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".media.CalendarActivity"
|
android:name=".media.CalendarActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
<activity android:name="ani.dantotsu.media.user.ListActivity" />
|
<activity android:name=".media.user.ListActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity"
|
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/manga"
|
android:label="@string/manga"
|
||||||
@@ -116,7 +131,7 @@
|
|||||||
<activity android:name=".media.CharacterDetailsActivity" />
|
<activity android:name=".media.CharacterDetailsActivity" />
|
||||||
<activity android:name=".home.NoInternet" />
|
<activity android:name=".home.NoInternet" />
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.media.anime.ExoplayerView"
|
android:name=".media.anime.ExoplayerView"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -125,7 +140,7 @@
|
|||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
tools:targetApi="n" />
|
tools:targetApi="n" />
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.connections.anilist.Login"
|
android:name=".connections.anilist.Login"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -142,7 +157,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.connections.mal.Login"
|
android:name=".connections.mal.Login"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -158,8 +173,8 @@
|
|||||||
android:scheme="dantotsu" />
|
android:scheme="dantotsu" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
<activity android:name="ani.dantotsu.connections.discord.Login"
|
android:name=".connections.discord.Login"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -170,15 +185,32 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="dantotsu"/>
|
<data android:scheme="dantotsu" />
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
<data android:host="discord.dantotsu.com"/>
|
<data android:host="discord.dantotsu.com" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="ani.dantotsu.connections.anilist.UrlMedia"
|
android:name=".others.webview.CookieCatcher"
|
||||||
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask">
|
||||||
|
<intent-filter android:label="Discord Login for Dantotsu">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="dantotsu" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="discord.dantotsu.com" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".connections.anilist.UrlMedia"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -214,30 +246,30 @@
|
|||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.Main" />
|
<action android:name="android.intent.action.Main" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".download.DownloadContainerActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".subcriptions.AlarmReceiver"
|
android:name=".subcriptions.AlarmReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="Aani.dantotsu.ACTION_ALARM"/>
|
<action android:name="Aani.dantotsu.ACTION_ALARM" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
@@ -256,24 +288,52 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service android:name=".download.video.MyDownloadService"
|
<service
|
||||||
android:exported="false">
|
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
|
<service
|
||||||
|
android:name=".download.video.ExoplayerDownloadService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
|
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
<service
|
||||||
android:foregroundServiceType="dataSync"
|
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||||
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"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name=".download.manga.MangaDownloaderService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name=".download.novel.NovelDownloaderService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name=".download.anime.AnimeDownloaderService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name=".connections.discord.DiscordService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||||
|
android: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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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 |
BIN
app/src/main/ic_launcher_beta-playstore.png
Normal file
BIN
app/src/main/ic_launcher_beta-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -8,14 +8,17 @@ import androidx.multidex.MultiDex
|
|||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import tachiyomi.core.util.system.logcat
|
|
||||||
import ani.dantotsu.others.DisabledReports
|
import ani.dantotsu.others.DisabledReports
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.MangaSources
|
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.android.material.color.DynamicColors
|
||||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import com.google.firebase.ktx.Firebase
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -25,14 +28,16 @@ import kotlinx.coroutines.launch
|
|||||||
import logcat.AndroidLogcatLogger
|
import logcat.AndroidLogcatLogger
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.LogcatLogger
|
import logcat.LogcatLogger
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
class App : MultiDexApplication() {
|
class App : MultiDexApplication() {
|
||||||
private lateinit var animeExtensionManager: AnimeExtensionManager
|
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||||
private lateinit var mangaExtensionManager: MangaExtensionManager
|
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||||
|
private lateinit var novelExtensionManager: NovelExtensionManager
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
MultiDex.install(this)
|
MultiDex.install(this)
|
||||||
@@ -46,18 +51,37 @@ class App : MultiDexApplication() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
|
||||||
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
|
PrefManager.init(this)
|
||||||
if(useMaterialYou) {
|
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)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
}
|
}
|
||||||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||||
|
|
||||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||||
initializeNetwork(baseContext)
|
(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)
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
if (!LogcatLogger.isInstalled) {
|
if (!LogcatLogger.isInstalled) {
|
||||||
@@ -66,6 +90,7 @@ class App : MultiDexApplication() {
|
|||||||
|
|
||||||
animeExtensionManager = Injekt.get()
|
animeExtensionManager = Injekt.get()
|
||||||
mangaExtensionManager = Injekt.get()
|
mangaExtensionManager = Injekt.get()
|
||||||
|
novelExtensionManager = Injekt.get()
|
||||||
|
|
||||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||||
animeScope.launch {
|
animeScope.launch {
|
||||||
@@ -79,9 +104,15 @@ class App : MultiDexApplication() {
|
|||||||
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||||
}
|
}
|
||||||
|
val novelScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
novelScope.launch {
|
||||||
|
novelExtensionManager.findAvailableExtensions()
|
||||||
|
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||||
|
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun setupNotificationChannels() {
|
private fun setupNotificationChannels() {
|
||||||
try {
|
try {
|
||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
@@ -109,7 +140,7 @@ class App : MultiDexApplication() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: App? = null
|
private var instance: App? = null
|
||||||
var context : Context? = null
|
var context: Context? = null
|
||||||
fun currentContext(): Context? {
|
fun currentContext(): Context? {
|
||||||
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import android.animation.ObjectAnimator
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.DatePickerDialog
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources.getSystem
|
import android.content.res.Resources.getSystem
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.Manifest
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities.*
|
import android.net.NetworkCapabilities.*
|
||||||
@@ -23,11 +27,16 @@ import android.telephony.TelephonyManager
|
|||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.animation.*
|
import android.view.animation.*
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
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.ContextCompat.getSystemService
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.math.MathUtils.clamp
|
import androidx.core.math.MathUtils.clamp
|
||||||
@@ -40,10 +49,15 @@ import androidx.viewpager2.widget.ViewPager2
|
|||||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||||
import ani.dantotsu.connections.anilist.Genre
|
import ani.dantotsu.connections.anilist.Genre
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.parsers.ShowResponse
|
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.Glide
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
||||||
@@ -53,8 +67,11 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import com.google.android.material.internal.ViewUtils
|
import com.google.android.material.internal.ViewUtils
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.lang.Runnable
|
import java.lang.Runnable
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
@@ -96,59 +113,27 @@ fun logger(e: Any?, print: Boolean = true) {
|
|||||||
println(e)
|
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) {
|
fun initActivity(a: Activity) {
|
||||||
val window = a.window
|
val window = a.window
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false) ?: UserInterfaceSettings().apply {
|
val darkMode = PrefManager.getVal<Int>(PrefName.DarkMode)
|
||||||
saveData("ui_settings", this)
|
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
||||||
}
|
darkMode.apply {
|
||||||
uiSettings.darkMode.apply {
|
|
||||||
AppCompatDelegate.setDefaultNightMode(
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
when (this) {
|
when (this) {
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
2 -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
1 -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (uiSettings.immersiveMode) {
|
if (immersiveMode) {
|
||||||
if (navBarHeight == 0) {
|
if (navBarHeight == 0) {
|
||||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))?.apply {
|
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
?.apply {
|
||||||
}
|
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||||
|
}
|
||||||
}
|
}
|
||||||
a.hideStatusBar()
|
a.hideStatusBar()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
@@ -160,7 +145,8 @@ fun initActivity(a: Activity) {
|
|||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
if (statusBarHeight == 0) {
|
if (statusBarHeight == 0) {
|
||||||
val windowInsets = ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
val windowInsets =
|
||||||
|
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||||
if (windowInsets != null) {
|
if (windowInsets != null) {
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
statusBarHeight = insets.top
|
statusBarHeight = insets.top
|
||||||
@@ -195,6 +181,14 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
}
|
}
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
val theme = requireContext().theme
|
||||||
|
theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSurface,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
window.navigationBarColor = typedValue.data
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun show(manager: FragmentManager, tag: String?) {
|
override fun show(manager: FragmentManager, tag: String?) {
|
||||||
@@ -205,7 +199,8 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isOnline(context: Context): Boolean {
|
fun isOnline(context: Context): Boolean {
|
||||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
val connectivityManager =
|
||||||
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
return tryWith {
|
return tryWith {
|
||||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||||
return@tryWith if (cap != null) {
|
return@tryWith if (cap != null) {
|
||||||
@@ -219,7 +214,7 @@ fun isOnline(context: Context): Boolean {
|
|||||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
} else false
|
} else false
|
||||||
} ?: false
|
} ?: false
|
||||||
@@ -239,7 +234,8 @@ fun startMainActivity(activity: Activity, bundle: Bundle? = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) : DialogFragment(),
|
class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) :
|
||||||
|
DialogFragment(),
|
||||||
DatePickerDialog.OnDateSetListener {
|
DatePickerDialog.OnDateSetListener {
|
||||||
var dialog: DatePickerDialog
|
var dialog: DatePickerDialog
|
||||||
|
|
||||||
@@ -264,9 +260,20 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputFilterMinMax(private val min: Double, private val max: Double, private val status: AutoCompleteTextView? = null) :
|
class InputFilterMinMax(
|
||||||
|
private val min: Double,
|
||||||
|
private val max: Double,
|
||||||
|
private val status: AutoCompleteTextView? = null
|
||||||
|
) :
|
||||||
InputFilter {
|
InputFilter {
|
||||||
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
|
override fun filter(
|
||||||
|
source: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
dest: Spanned,
|
||||||
|
dstart: Int,
|
||||||
|
dend: Int
|
||||||
|
): CharSequence? {
|
||||||
try {
|
try {
|
||||||
val input = (dest.toString() + source.toString()).toDouble()
|
val input = (dest.toString() + source.toString()).toDouble()
|
||||||
if (isInRange(min, max, input)) return null
|
if (isInRange(min, max, input)) return null
|
||||||
@@ -289,11 +296,20 @@ class InputFilterMinMax(private val min: Double, private val max: Double, privat
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : ViewPager2.PageTransformer {
|
class ZoomOutPageTransformer() :
|
||||||
|
ViewPager2.PageTransformer {
|
||||||
override fun transformPage(view: View, position: Float) {
|
override fun transformPage(view: View, position: Float) {
|
||||||
if (position == 0.0f && uiSettings.layoutAnimations) {
|
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||||
setAnimation(view.context, view, uiSettings, 300, floatArrayOf(1.3f, 1f, 1.3f, 1f), 0.5f to 0f)
|
setAnimation(
|
||||||
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f).setDuration((200 * uiSettings.animationSpeed).toLong()).start()
|
view.context,
|
||||||
|
view,
|
||||||
|
300,
|
||||||
|
floatArrayOf(1.3f, 1f, 1.3f, 1f),
|
||||||
|
0.5f to 0f
|
||||||
|
)
|
||||||
|
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f)
|
||||||
|
.setDuration((200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong())
|
||||||
|
.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,12 +317,11 @@ class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : Vi
|
|||||||
fun setAnimation(
|
fun setAnimation(
|
||||||
context: Context,
|
context: Context,
|
||||||
viewToAnimate: View,
|
viewToAnimate: View,
|
||||||
uiSettings: UserInterfaceSettings,
|
|
||||||
duration: Long = 150,
|
duration: Long = 150,
|
||||||
list: FloatArray = floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f),
|
list: FloatArray = floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f),
|
||||||
pivot: Pair<Float, Float> = 0.5f to 0.5f
|
pivot: Pair<Float, Float> = 0.5f to 0.5f
|
||||||
) {
|
) {
|
||||||
if (uiSettings.layoutAnimations) {
|
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||||
val anim = ScaleAnimation(
|
val anim = ScaleAnimation(
|
||||||
list[0],
|
list[0],
|
||||||
list[1],
|
list[1],
|
||||||
@@ -317,7 +332,7 @@ fun setAnimation(
|
|||||||
Animation.RELATIVE_TO_SELF,
|
Animation.RELATIVE_TO_SELF,
|
||||||
pivot.second
|
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)
|
anim.setInterpolator(context, R.anim.over_shoot)
|
||||||
viewToAnimate.startAnimation(anim)
|
viewToAnimate.startAnimation(anim)
|
||||||
}
|
}
|
||||||
@@ -328,7 +343,11 @@ class FadingEdgeRecyclerView : RecyclerView {
|
|||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
)
|
||||||
|
|
||||||
override fun isPaddingOffsetRequired(): Boolean {
|
override fun isPaddingOffsetRequired(): Boolean {
|
||||||
return !clipToPadding
|
return !clipToPadding
|
||||||
@@ -414,7 +433,7 @@ fun MutableList<ShowResponse>.sortByTitle(string: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun String.findBetween(a: String, b: String): String? {
|
fun String.findBetween(a: String, b: String): String? {
|
||||||
val string = substringAfter(a, "").substringBefore(b,"")
|
val string = substringAfter(a, "").substringBefore(b, "")
|
||||||
return string.ifEmpty { null }
|
return string.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,8 +442,7 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
|
|||||||
val localFile = File(url)
|
val localFile = File(url)
|
||||||
if (localFile.exists()) {
|
if (localFile.exists()) {
|
||||||
loadLocalImage(localFile, size)
|
loadLocalImage(localFile, size)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
loadImage(FileUrl(url), size)
|
loadImage(FileUrl(url), size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,7 +452,8 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
|||||||
if (file?.url?.isNotEmpty() == true) {
|
if (file?.url?.isNotEmpty() == true) {
|
||||||
tryWith {
|
tryWith {
|
||||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size).into(this)
|
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||||
|
.into(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,7 +461,8 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
|||||||
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
||||||
if (file?.exists() == true) {
|
if (file?.exists() == true) {
|
||||||
tryWith {
|
tryWith {
|
||||||
Glide.with(this.context).load(file).transition(withCrossFade()).override(size).into(this)
|
Glide.with(this.context).load(file).transition(withCrossFade()).override(size)
|
||||||
|
.into(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +520,12 @@ abstract class GesturesListener : GestureDetector.SimpleOnGestureListener() {
|
|||||||
return super.onDoubleTap(e)
|
return super.onDoubleTap(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
override fun onScroll(
|
||||||
|
e1: MotionEvent?,
|
||||||
|
e2: MotionEvent,
|
||||||
|
distanceX: Float,
|
||||||
|
distanceY: Float
|
||||||
|
): Boolean {
|
||||||
onScrollYClick(distanceY)
|
onScrollYClick(distanceY)
|
||||||
onScrollXClick(distanceX)
|
onScrollXClick(distanceX)
|
||||||
return super.onScroll(e1, e2, distanceX, distanceY)
|
return super.onScroll(e1, e2, distanceX, distanceY)
|
||||||
@@ -560,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(
|
FileProvider.getUriForFile(
|
||||||
context,
|
context,
|
||||||
"$APPLICATION_ID.provider",
|
"$APPLICATION_ID.provider",
|
||||||
saveImage(
|
saveImage(
|
||||||
bitmap,
|
bitmap,
|
||||||
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS,
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||||
title
|
title
|
||||||
) ?: return
|
) ?: return
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||||
|
|
||||||
val contentUri = FileProvider.getUriForFile(
|
val contentUri = FileProvider.getUriForFile(
|
||||||
@@ -589,13 +713,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
|||||||
|
|
||||||
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
||||||
val imageFile = File(path, "$imageFileName.png")
|
val imageFile = File(path, "$imageFileName.png")
|
||||||
return tryWith {
|
return try {
|
||||||
val fOut: OutputStream = FileOutputStream(imageFile)
|
val fOut: OutputStream = FileOutputStream(imageFile)
|
||||||
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
|
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
|
||||||
fOut.close()
|
fOut.close()
|
||||||
scanFile(imageFile.absolutePath, currContext()!!)
|
scanFile(imageFile.absolutePath, currContext()!!)
|
||||||
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
|
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
|
||||||
imageFile
|
imageFile
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snackString("Failed to save image: ${e.localizedMessage}")
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,13 +765,19 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
fun countDown(media: Media, view: ViewGroup) {
|
fun countDown(media: Media, view: ViewGroup) {
|
||||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) {
|
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
|
||||||
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||||
view.addView(v.root, 0)
|
view.addView(v.root, 0)
|
||||||
v.mediaCountdownText.text =
|
v.mediaCountdownText.text =
|
||||||
currActivity()?.getString(R.string.episode_release_countdown, media.anime.nextAiringEpisode!! + 1)
|
currActivity()?.getString(
|
||||||
|
R.string.episode_release_countdown,
|
||||||
|
media.anime.nextAiringEpisode!! + 1
|
||||||
|
)
|
||||||
|
|
||||||
object : CountDownTimer((media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(), 1000) {
|
object : CountDownTimer(
|
||||||
|
(media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(),
|
||||||
|
1000
|
||||||
|
) {
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
val a = millisUntilFinished / 1000
|
val a = millisUntilFinished / 1000
|
||||||
v.mediaCountdown.text = currActivity()?.getString(
|
v.mediaCountdown.text = currActivity()?.getString(
|
||||||
@@ -679,10 +812,11 @@ fun MutableMap<String, Genre>.checkGenreTime(genre: String): Boolean {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSlideIn(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
fun setSlideIn() = AnimationSet(false).apply {
|
||||||
if (uiSettings.layoutAnimations) {
|
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||||
var animation: Animation = AlphaAnimation(0.0f, 1.0f)
|
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()
|
animation.interpolator = AccelerateDecelerateInterpolator()
|
||||||
addAnimation(animation)
|
addAnimation(animation)
|
||||||
|
|
||||||
@@ -693,16 +827,17 @@ fun setSlideIn(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
|||||||
Animation.RELATIVE_TO_SELF, 0f
|
Animation.RELATIVE_TO_SELF, 0f
|
||||||
)
|
)
|
||||||
|
|
||||||
animation.duration = (750 * uiSettings.animationSpeed).toLong()
|
animation.duration = (750 * animationSpeed).toLong()
|
||||||
animation.interpolator = OvershootInterpolator(1.1f)
|
animation.interpolator = OvershootInterpolator(1.1f)
|
||||||
addAnimation(animation)
|
addAnimation(animation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSlideUp(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
fun setSlideUp() = AnimationSet(false).apply {
|
||||||
if (uiSettings.layoutAnimations) {
|
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||||
var animation: Animation = AlphaAnimation(0.0f, 1.0f)
|
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()
|
animation.interpolator = AccelerateDecelerateInterpolator()
|
||||||
addAnimation(animation)
|
addAnimation(animation)
|
||||||
|
|
||||||
@@ -713,7 +848,7 @@ fun setSlideUp(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
|||||||
Animation.RELATIVE_TO_SELF, 0f
|
Animation.RELATIVE_TO_SELF, 0f
|
||||||
)
|
)
|
||||||
|
|
||||||
animation.duration = (750 * uiSettings.animationSpeed).toLong()
|
animation.duration = (750 * animationSpeed).toLong()
|
||||||
animation.interpolator = OvershootInterpolator(1.1f)
|
animation.interpolator = OvershootInterpolator(1.1f)
|
||||||
addAnimation(animation)
|
addAnimation(animation)
|
||||||
}
|
}
|
||||||
@@ -735,41 +870,52 @@ fun toast(string: String?) {
|
|||||||
if (string != null) {
|
if (string != null) {
|
||||||
logger(string)
|
logger(string)
|
||||||
MainScope().launch {
|
MainScope().launch {
|
||||||
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT).show()
|
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
|
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
|
||||||
if (s != null) {
|
try { //I have no idea why this sometimes crashes for some people...
|
||||||
(activity ?: currActivity())?.apply {
|
if (s != null) {
|
||||||
runOnUiThread {
|
(activity ?: currActivity())?.apply {
|
||||||
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_SHORT)
|
runOnUiThread {
|
||||||
snackBar.view.apply {
|
val snackBar = Snackbar.make(
|
||||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
window.decorView.findViewById(android.R.id.content),
|
||||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
s,
|
||||||
width = WRAP_CONTENT
|
Snackbar.LENGTH_SHORT
|
||||||
}
|
)
|
||||||
translationY = -(navBarHeight.dp + 32f)
|
snackBar.view.apply {
|
||||||
translationZ = 32f
|
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||||
updatePadding(16f.px, right = 16f.px)
|
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||||
setOnClickListener {
|
width = WRAP_CONTENT
|
||||||
snackBar.dismiss()
|
}
|
||||||
}
|
translationY = -(navBarHeight.dp + 32f)
|
||||||
setOnLongClickListener {
|
translationZ = 32f
|
||||||
copyToClipboard(clipboard ?: s, false)
|
updatePadding(16f.px, right = 16f.px)
|
||||||
toast(getString(R.string.copied_to_clipboard))
|
setOnClickListener {
|
||||||
true
|
snackBar.dismiss()
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
copyToClipboard(clipboard ?: s, false)
|
||||||
|
toast(getString(R.string.copied_to_clipboard))
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
snackBar.show()
|
||||||
}
|
}
|
||||||
snackBar.show()
|
|
||||||
}
|
}
|
||||||
|
logger(s)
|
||||||
}
|
}
|
||||||
logger(s)
|
} catch (e: Exception) {
|
||||||
|
logger(e.stackTraceToString())
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) : ArrayAdapter<T>(context, layoutId, items) {
|
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
|
||||||
|
ArrayAdapter<T>(context, layoutId, items) {
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
val view = super.getView(position, convertView, parent)
|
val view = super.getView(position, convertView, parent)
|
||||||
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
|
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
|
||||||
@@ -790,16 +936,21 @@ class SpinnerNoSwipe : androidx.appcompat.widget.AppCompatSpinner {
|
|||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
) {
|
||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setup() {
|
private fun setup() {
|
||||||
mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
mGestureDetector =
|
||||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
return performClick()
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
}
|
return performClick()
|
||||||
})
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
@@ -843,7 +994,11 @@ fun getCurrentBrightnessValue(context: Context): Float {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCur(): Float {
|
fun getCur(): Float {
|
||||||
return Settings.System.getInt(context.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 127).toFloat()
|
return Settings.System.getInt(
|
||||||
|
context.contentResolver,
|
||||||
|
Settings.System.SCREEN_BRIGHTNESS,
|
||||||
|
127
|
||||||
|
).toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
return brightnessConverter(getCur() / getMax(), true)
|
return brightnessConverter(getCur() / getMax(), true)
|
||||||
@@ -865,12 +1020,38 @@ fun checkCountry(context: Context): Boolean {
|
|||||||
tz.equals("Asia/Kolkata", ignoreCase = true)
|
tz.equals("Asia/Kolkata", ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
TelephonyManager.SIM_STATE_READY -> {
|
TelephonyManager.SIM_STATE_READY -> {
|
||||||
val countryCodeValue = telMgr.networkCountryIso
|
val countryCodeValue = telMgr.networkCountryIso
|
||||||
countryCodeValue.equals("in", ignoreCase = true)
|
countryCodeValue.equals("in", ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val INCOGNITO_CHANNEL_ID = 26
|
||||||
|
|
||||||
|
@SuppressLint("LaunchActivityFromNotification")
|
||||||
|
fun incognitoNotification(context: Context) {
|
||||||
|
val notificationManager =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val incognito: 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,8 @@ package ani.dantotsu
|
|||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -21,14 +15,13 @@ import android.util.Log
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnticipateInterpolator
|
import android.view.animation.AnticipateInterpolator
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.doOnAttach
|
import androidx.core.view.doOnAttach
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
@@ -36,14 +29,14 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.offline.Download
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||||
import ani.dantotsu.databinding.ActivityMainBinding
|
import ani.dantotsu.databinding.ActivityMainBinding
|
||||||
import ani.dantotsu.databinding.ItemNavbarBinding
|
|
||||||
import ani.dantotsu.databinding.SplashScreenBinding
|
import ani.dantotsu.databinding.SplashScreenBinding
|
||||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
import ani.dantotsu.download.video.Helper
|
||||||
import ani.dantotsu.home.AnimeFragment
|
import ani.dantotsu.home.AnimeFragment
|
||||||
import ani.dantotsu.home.HomeFragment
|
import ani.dantotsu.home.HomeFragment
|
||||||
import ani.dantotsu.home.LoginFragment
|
import ani.dantotsu.home.LoginFragment
|
||||||
@@ -51,42 +44,43 @@ import ani.dantotsu.home.MangaFragment
|
|||||||
import ani.dantotsu.home.NoInternet
|
import ani.dantotsu.home.NoInternet
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.others.CustomBottomDialog
|
import ani.dantotsu.others.CustomBottomDialog
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||||
import ani.dantotsu.settings.SettingsActivity
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
|
||||||
private val scope = lifecycleScope
|
private val scope = lifecycleScope
|
||||||
private var load = false
|
private var load = false
|
||||||
|
|
||||||
private var uiSettings = UserInterfaceSettings()
|
|
||||||
|
|
||||||
|
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
//get FRAGMENT_CLASS_NAME from intent
|
||||||
|
val fragment = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
@@ -95,17 +89,54 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
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)
|
backgroundDrawable.setColor(semiTransparentColor)
|
||||||
_bottomBar.background = backgroundDrawable
|
_bottomBar.background = backgroundDrawable
|
||||||
}
|
}
|
||||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||||
.getBoolean("colorOverflow", false)
|
|
||||||
if (!colorOverflow) {
|
|
||||||
_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
|
var doubleBackToExitPressedOnce = false
|
||||||
onBackPressedDispatcher.addCallback(this) {
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
@@ -120,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
|
binding.root.isMotionEventSplittingEnabled = false
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -163,111 +205,146 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
binding.root.doOnAttach {
|
binding.root.doOnAttach {
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
selectedOption = if (fragment != null) {
|
||||||
selectedOption = uiSettings.defaultStartUpTab
|
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> {
|
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = navBarHeight
|
bottomMargin = navBarHeight
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||||
if (!isOnline(this)) {
|
if (!isOnline(this)) {
|
||||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||||
startActivity(Intent(this, NoInternet::class.java))
|
startActivity(Intent(this, NoInternet::class.java))
|
||||||
} else {
|
} else {
|
||||||
val model: AnilistHomeViewModel by viewModels()
|
if (offlineMode) {
|
||||||
model.genres.observe(this) {
|
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||||
if (it != null) {
|
startActivity(Intent(this, NoInternet::class.java))
|
||||||
if (it) {
|
} else {
|
||||||
val navbar = binding.includedNavbar.navbar
|
val model: AnilistHomeViewModel by viewModels()
|
||||||
bottomBar = navbar
|
model.genres.observe(this) { it ->
|
||||||
navbar.visibility = View.VISIBLE
|
if (it != null) {
|
||||||
binding.mainProgressBar.visibility = View.GONE
|
if (it) {
|
||||||
val mainViewPager = binding.viewpager
|
val navbar = binding.includedNavbar.navbar
|
||||||
mainViewPager.isUserInputEnabled = false
|
bottomBar = navbar
|
||||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
navbar.visibility = View.VISIBLE
|
||||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
binding.mainProgressBar.visibility = View.GONE
|
||||||
navbar.setOnTabSelectListener(object :
|
val mainViewPager = binding.viewpager
|
||||||
AnimatedBottomBar.OnTabSelectListener {
|
mainViewPager.isUserInputEnabled = false
|
||||||
override fun onTabSelected(
|
mainViewPager.adapter =
|
||||||
lastIndex: Int,
|
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||||
lastTab: AnimatedBottomBar.Tab?,
|
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||||
newIndex: Int,
|
navbar.setOnTabSelectListener(object :
|
||||||
newTab: AnimatedBottomBar.Tab
|
AnimatedBottomBar.OnTabSelectListener {
|
||||||
) {
|
override fun onTabSelected(
|
||||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
lastIndex: Int,
|
||||||
selectedOption = newIndex
|
lastTab: AnimatedBottomBar.Tab?,
|
||||||
mainViewPager.setCurrentItem(newIndex, false)
|
newIndex: Int,
|
||||||
}
|
newTab: AnimatedBottomBar.Tab
|
||||||
})
|
) {
|
||||||
navbar.selectTabAt(selectedOption)
|
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||||
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
|
selectedOption = newIndex
|
||||||
} else {
|
mainViewPager.setCurrentItem(newIndex, false)
|
||||||
binding.mainProgressBar.visibility = View.GONE
|
}
|
||||||
}
|
})
|
||||||
}
|
navbar.selectTabAt(selectedOption)
|
||||||
}
|
mainViewPager.post {
|
||||||
//Load Data
|
mainViewPager.setCurrentItem(
|
||||||
if (!load) {
|
selectedOption,
|
||||||
scope.launch(Dispatchers.IO) {
|
false
|
||||||
model.loadMain(this@MainActivity)
|
|
||||||
val id = intent.extras?.getInt("mediaId", 0)
|
|
||||||
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
|
||||||
val cont = intent.extras?.getBoolean("continue") ?: false
|
|
||||||
if (id != null && id != 0) {
|
|
||||||
val media = withContext(Dispatchers.IO) {
|
|
||||||
Anilist.query.getMedia(id, isMAL)
|
|
||||||
}
|
|
||||||
if (media != null) {
|
|
||||||
media.cameFromContinue = cont
|
|
||||||
startActivity(
|
|
||||||
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
|
||||||
.putExtra("media", media as Serializable)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delay(500)
|
|
||||||
startSubscription()
|
|
||||||
}
|
|
||||||
load = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
if (loadData<Boolean>("allow_opening_links", this) != true) {
|
|
||||||
CustomBottomDialog.newInstance().apply {
|
|
||||||
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
|
||||||
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
val markWon =
|
|
||||||
Markwon.builder(this@MainActivity)
|
|
||||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
|
||||||
markWon.setMarkdown(this, md)
|
|
||||||
})
|
|
||||||
|
|
||||||
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
|
||||||
saveData("allow_opening_links", true, this@MainActivity)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
|
||||||
saveData("allow_opening_links", true, this@MainActivity)
|
|
||||||
tryWith(true) {
|
|
||||||
startActivity(
|
|
||||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
|
||||||
.setData(Uri.parse("package:$packageName"))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
binding.mainProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}.show(supportFragmentManager, "dialog")
|
}
|
||||||
|
}
|
||||||
|
//Load Data
|
||||||
|
if (!load) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
model.loadMain(this@MainActivity)
|
||||||
|
val id = intent.extras?.getInt("mediaId", 0)
|
||||||
|
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
||||||
|
val cont = intent.extras?.getBoolean("continue") ?: false
|
||||||
|
if (id != null && id != 0) {
|
||||||
|
val media = withContext(Dispatchers.IO) {
|
||||||
|
Anilist.query.getMedia(id, isMAL)
|
||||||
|
}
|
||||||
|
if (media != null) {
|
||||||
|
media.cameFromContinue = cont
|
||||||
|
startActivity(
|
||||||
|
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
||||||
|
.putExtra("media", media as Serializable)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(500)
|
||||||
|
startSubscription()
|
||||||
|
}
|
||||||
|
load = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if (!(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)
|
||||||
override fun onResume() {
|
}
|
||||||
super.onResume()
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,58 +8,51 @@ import ani.dantotsu.others.webview.WebViewBottomDialog
|
|||||||
import com.lagradost.nicehttp.Requests
|
import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.ResponseParser
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
import com.lagradost.nicehttp.addGenericDns
|
import com.lagradost.nicehttp.addGenericDns
|
||||||
import kotlinx.coroutines.*
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.InternalSerializationApi
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import okhttp3.Cache
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.io.File
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KFunction
|
import kotlin.reflect.KFunction
|
||||||
|
|
||||||
val defaultHeaders = mapOf(
|
lateinit var defaultHeaders: Map<String, String>
|
||||||
"User-Agent" to
|
|
||||||
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
|
|
||||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
|
||||||
)
|
|
||||||
lateinit var cache: Cache
|
|
||||||
|
|
||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var okHttpClient: OkHttpClient
|
||||||
lateinit var client: Requests
|
lateinit var client: Requests
|
||||||
|
|
||||||
fun initializeNetwork(context: Context) {
|
fun initializeNetwork(context: Context) {
|
||||||
val dns = loadData<Int>("settings_dns")
|
|
||||||
cache = Cache(
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
File(context.cacheDir, "http_cache"),
|
|
||||||
5 * 1024L * 1024L // 5 MiB
|
defaultHeaders = mapOf(
|
||||||
|
"User-Agent" to
|
||||||
|
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
||||||
|
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||||
)
|
)
|
||||||
okHttpClient = OkHttpClient.Builder()
|
|
||||||
.followRedirects(true)
|
okHttpClient = networkHelper.client
|
||||||
.followSslRedirects(true)
|
|
||||||
.cache(cache)
|
|
||||||
.apply {
|
|
||||||
when (dns) {
|
|
||||||
1 -> addGoogleDns()
|
|
||||||
2 -> addCloudFlareDns()
|
|
||||||
3 -> addAdGuardDns()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
client = Requests(
|
client = Requests(
|
||||||
okHttpClient,
|
networkHelper.client,
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
defaultCacheTime = 6,
|
defaultCacheTime = 6,
|
||||||
defaultCacheTimeUnit = TimeUnit.HOURS,
|
defaultCacheTimeUnit = TimeUnit.HOURS,
|
||||||
responseParser = Mapper
|
responseParser = Mapper
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Mapper : ResponseParser {
|
object Mapper : ResponseParser {
|
||||||
@@ -122,7 +115,11 @@ fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> tryWithSuspend(post: Boolean = false, snackbar: Boolean = true, call: suspend () -> T): T? {
|
suspend fun <T> tryWithSuspend(
|
||||||
|
post: Boolean = false,
|
||||||
|
snackbar: Boolean = true,
|
||||||
|
call: suspend () -> T
|
||||||
|
): T? {
|
||||||
return try {
|
return try {
|
||||||
call.invoke()
|
call.invoke()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -202,28 +199,29 @@ fun OkHttpClient.Builder.addAdGuardDns() = (
|
|||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, String>? {
|
suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, String>? {
|
||||||
var map : Map<String,String>? = null
|
var map: Map<String, String>? = null
|
||||||
|
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
webViewDialog.callback = {
|
webViewDialog.callback = {
|
||||||
map = it
|
map = it
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
val fragmentManager = (currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
|
val fragmentManager =
|
||||||
|
(currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
|
||||||
webViewDialog.show(fragmentManager, "web-view")
|
webViewDialog.show(fragmentManager, "web-view")
|
||||||
delay(0)
|
delay(0)
|
||||||
latch.await(2,TimeUnit.MINUTES)
|
latch.await(2, TimeUnit.MINUTES)
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun webViewInterface(type: String, url: FileUrl): Map<String, String>? {
|
suspend fun webViewInterface(type: String, url: FileUrl): Map<String, String>? {
|
||||||
val webViewDialog: WebViewBottomDialog = when (type) {
|
val webViewDialog: WebViewBottomDialog = when (type) {
|
||||||
"Cloudflare" -> CloudFlare.newInstance(url)
|
"Cloudflare" -> CloudFlare.newInstance(url)
|
||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
return webViewInterface(webViewDialog)
|
return webViewInterface(webViewDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun webViewInterface(type: String, url: String): Map<String, String>? {
|
suspend fun webViewInterface(type: String, url: String): Map<String, String>? {
|
||||||
return webViewInterface(type,FileUrl(url))
|
return webViewInterface(type, FileUrl(url))
|
||||||
}
|
}
|
||||||
@@ -2,20 +2,24 @@ package ani.dantotsu.aniyomi.anime.custom
|
|||||||
|
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.content.ContextCompat
|
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.media.manga.MangaCache
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
|
||||||
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||||
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
@@ -23,25 +27,23 @@ import uy.kohesive.injekt.api.InjektRegistrar
|
|||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import ani.dantotsu.download.DownloadsManager
|
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
addSingletonFactory { DownloadsManager(app) }
|
addSingletonFactory { DownloadsManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { NetworkHelper(app, get()) }
|
addSingletonFactory { NetworkHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { AnimeExtensionManager(app) }
|
addSingletonFactory { AnimeExtensionManager(app) }
|
||||||
addSingletonFactory { MangaExtensionManager(app) }
|
addSingletonFactory { MangaExtensionManager(app) }
|
||||||
|
addSingletonFactory { NovelExtensionManager(app) }
|
||||||
|
|
||||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||||
|
|
||||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
|
||||||
addSingleton(sharedPreferences)
|
|
||||||
|
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
Json {
|
Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
@@ -49,6 +51,12 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSingletonFactory { StandaloneDatabaseProvider(app) }
|
||||||
|
|
||||||
|
addSingletonFactory<CrashlyticsInterface> {
|
||||||
|
ani.dantotsu.connections.crashlytics.CrashlyticsFactory.createCrashlytics()
|
||||||
|
}
|
||||||
|
|
||||||
addSingletonFactory { MangaCache() }
|
addSingletonFactory { MangaCache() }
|
||||||
|
|
||||||
ContextCompat.getMainExecutor(app).execute {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
@@ -64,13 +72,6 @@ class PreferenceModule(val application: Application) : InjektModule {
|
|||||||
AndroidPreferenceStore(application)
|
AndroidPreferenceStore(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
addSingletonFactory {
|
|
||||||
NetworkPreferences(
|
|
||||||
preferenceStore = get(),
|
|
||||||
verboseLogging = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
SourcePreferences(get())
|
SourcePreferences(get())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,34 +6,40 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
fun updateProgress(media: Media, number: String) {
|
fun updateProgress(media: Media, number: String) {
|
||||||
if (Anilist.userid != null) {
|
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
if (!incognito) {
|
||||||
val a = number.toFloatOrNull()?.roundToInt()
|
if (Anilist.userid != null) {
|
||||||
if ((a?:0) > (media.userProgress?:0)) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
Anilist.mutation.editList(
|
val a = number.toFloatOrNull()?.toInt()
|
||||||
media.id,
|
if ((a ?: 0) > (media.userProgress ?: 0)) {
|
||||||
a,
|
Anilist.mutation.editList(
|
||||||
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
media.id,
|
||||||
)
|
a,
|
||||||
MAL.query.editList(
|
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
||||||
media.idMAL,
|
)
|
||||||
media.anime != null,
|
MAL.query.editList(
|
||||||
a, null,
|
media.idMAL,
|
||||||
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
media.anime != null,
|
||||||
)
|
a, null,
|
||||||
toast(currContext()?.getString(R.string.setting_progress, a))
|
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
||||||
|
)
|
||||||
|
toast(currContext()?.getString(R.string.setting_progress, a))
|
||||||
|
}
|
||||||
|
media.userProgress = a
|
||||||
|
Refresh.all()
|
||||||
}
|
}
|
||||||
media.userProgress = a
|
} else {
|
||||||
Refresh.all()
|
toast(currContext()?.getString(R.string.login_anilist_account))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast(currContext()?.getString(R.string.login_anilist_account))
|
toast("Sneaky sneaky :3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,14 +3,17 @@ package ani.dantotsu.connections.anilist
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.client
|
import ani.dantotsu.client
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.openLinkInBrowser
|
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 ani.dantotsu.tryWithSuspend
|
||||||
import java.io.File
|
import java.util.Calendar
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
object Anilist {
|
object Anilist {
|
||||||
val query: AnilistQueries = AnilistQueries()
|
val query: AnilistQueries = AnilistQueries()
|
||||||
@@ -28,8 +31,15 @@ object Anilist {
|
|||||||
var genres: ArrayList<String>? = null
|
var genres: ArrayList<String>? = null
|
||||||
var tags: Map<Boolean, List<String>>? = null
|
var tags: Map<Boolean, List<String>>? = null
|
||||||
|
|
||||||
|
var rateLimitReset: Long = 0
|
||||||
|
|
||||||
val sortBy = listOf(
|
val sortBy = listOf(
|
||||||
"SCORE_DESC","POPULARITY_DESC","TRENDING_DESC","TITLE_ENGLISH","TITLE_ENGLISH_DESC","SCORE"
|
"SCORE_DESC",
|
||||||
|
"POPULARITY_DESC",
|
||||||
|
"TRENDING_DESC",
|
||||||
|
"TITLE_ENGLISH",
|
||||||
|
"TITLE_ENGLISH_DESC",
|
||||||
|
"SCORE"
|
||||||
)
|
)
|
||||||
|
|
||||||
val seasons = listOf(
|
val seasons = listOf(
|
||||||
@@ -51,11 +61,11 @@ object Anilist {
|
|||||||
private val cal: Calendar = Calendar.getInstance()
|
private val cal: Calendar = Calendar.getInstance()
|
||||||
private val currentYear = cal.get(Calendar.YEAR)
|
private val currentYear = cal.get(Calendar.YEAR)
|
||||||
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
|
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
|
||||||
0, 1, 2 -> 0
|
0, 1, 2 -> 0
|
||||||
3, 4, 5 -> 1
|
3, 4, 5 -> 1
|
||||||
6, 7, 8 -> 2
|
6, 7, 8 -> 2
|
||||||
9, 10, 11 -> 3
|
9, 10, 11 -> 3
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSeason(next: Boolean): Pair<String, Int> {
|
private fun getSeason(next: Boolean): Pair<String, Int> {
|
||||||
@@ -89,15 +99,12 @@ object Anilist {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSavedToken(context: Context): Boolean {
|
fun getSavedToken(): Boolean {
|
||||||
if ("anilistToken" in context.fileList()) {
|
token = PrefManager.getVal(PrefName.AnilistToken, null as String?)
|
||||||
token = File(context.filesDir, "anilistToken").readText()
|
return !token.isNullOrEmpty()
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSavedToken(context: Context) {
|
fun removeSavedToken() {
|
||||||
token = null
|
token = null
|
||||||
username = null
|
username = null
|
||||||
adult = false
|
adult = false
|
||||||
@@ -106,9 +113,7 @@ object Anilist {
|
|||||||
bg = null
|
bg = null
|
||||||
episodesWatched = null
|
episodesWatched = null
|
||||||
chapterRead = null
|
chapterRead = null
|
||||||
if ("anilistToken" in context.fileList()) {
|
PrefManager.removeVal(PrefName.AnilistToken)
|
||||||
File(context.filesDir, "anilistToken").delete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> executeQuery(
|
suspend inline fun <reified T : Any> executeQuery(
|
||||||
@@ -120,6 +125,10 @@ object Anilist {
|
|||||||
cache: Int? = null
|
cache: Int? = null
|
||||||
): T? {
|
): T? {
|
||||||
return tryWithSuspend {
|
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(
|
val data = mapOf(
|
||||||
"query" to query,
|
"query" to query,
|
||||||
"variables" to variables
|
"variables" to variables
|
||||||
@@ -132,7 +141,24 @@ object Anilist {
|
|||||||
if (token != null || force) {
|
if (token != null || force) {
|
||||||
if (token != null && useToken) headers["Authorization"] = "Bearer $token"
|
if (token != null && useToken) headers["Authorization"] = "Bearer $token"
|
||||||
|
|
||||||
val json = client.post("https://graphql.anilist.co/", headers, data = data, cacheTime = cache ?: 10)
|
val json = client.post(
|
||||||
|
"https://graphql.anilist.co/",
|
||||||
|
headers,
|
||||||
|
data = data,
|
||||||
|
cacheTime = cache ?: 10
|
||||||
|
)
|
||||||
|
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 (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||||
if (show) println("Response : ${json.text}")
|
if (show) println("Response : ${json.text}")
|
||||||
json.parsed()
|
json.parsed()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class AnilistMutations {
|
|||||||
repeat: Int? = null,
|
repeat: Int? = null,
|
||||||
notes: String? = null,
|
notes: String? = null,
|
||||||
status: String? = null,
|
status: String? = null,
|
||||||
private:Boolean? = null,
|
private: Boolean? = null,
|
||||||
startedAt: FuzzyDate? = null,
|
startedAt: FuzzyDate? = null,
|
||||||
completedAt: FuzzyDate? = null,
|
completedAt: FuzzyDate? = null,
|
||||||
customList: List<String>? = null
|
customList: List<String>? = null
|
||||||
@@ -41,7 +41,7 @@ class AnilistMutations {
|
|||||||
${if (repeat != null) ""","repeat":$repeat""" else ""}
|
${if (repeat != null) ""","repeat":$repeat""" else ""}
|
||||||
${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""}
|
${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""}
|
||||||
${if (status != null) ""","status":"$status"""" else ""}
|
${if (status != null) ""","status":"$status"""" else ""}
|
||||||
${if (customList !=null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""}
|
${if (customList != null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""}
|
||||||
}""".replace("\n", "").replace(""" """, "")
|
}""".replace("\n", "").replace(""" """, "")
|
||||||
println(variables)
|
println(variables)
|
||||||
executeQuery<JsonObject>(query, variables, show = true)
|
executeQuery<JsonObject>(query, variables, show = true)
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
package ani.dantotsu.connections.anilist
|
package ani.dantotsu.connections.anilist
|
||||||
|
|
||||||
import android.app.Activity
|
import android.util.Base64
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.checkGenreTime
|
||||||
|
import ani.dantotsu.checkId
|
||||||
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
||||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.connections.anilist.api.Page
|
import ani.dantotsu.connections.anilist.api.Page
|
||||||
import ani.dantotsu.connections.anilist.api.Query
|
import ani.dantotsu.connections.anilist.api.Query
|
||||||
import ani.dantotsu.checkGenreTime
|
|
||||||
import ani.dantotsu.checkId
|
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.isOnline
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.media.Author
|
import ani.dantotsu.media.Author
|
||||||
import ani.dantotsu.media.Character
|
import ani.dantotsu.media.Character
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.Studio
|
import ani.dantotsu.media.Studio
|
||||||
import ani.dantotsu.others.MalScraper
|
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 ani.dantotsu.snackString
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
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
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class AnilistQueries {
|
class AnilistQueries {
|
||||||
@@ -33,6 +39,8 @@ class AnilistQueries {
|
|||||||
}.also { println("time : $it") }
|
}.also { println("time : $it") }
|
||||||
val user = response?.data?.user ?: return false
|
val user = response?.data?.user ?: return false
|
||||||
|
|
||||||
|
PrefManager.setVal(PrefName.AnilistUserName, user.name)
|
||||||
|
|
||||||
Anilist.userid = user.id
|
Anilist.userid = user.id
|
||||||
Anilist.username = user.name
|
Anilist.username = user.name
|
||||||
Anilist.bg = user.bannerImage
|
Anilist.bg = user.bannerImage
|
||||||
@@ -113,9 +121,13 @@ class AnilistQueries {
|
|||||||
name = i.node?.name?.userPreferred,
|
name = i.node?.name?.userPreferred,
|
||||||
image = i.node?.image?.medium,
|
image = i.node?.image?.medium,
|
||||||
banner = media.banner ?: media.cover,
|
banner = media.banner ?: media.cover,
|
||||||
role = when (i.role.toString()){
|
role = when (i.role.toString()) {
|
||||||
"MAIN" -> currContext()?.getString(R.string.main_role) ?: "MAIN"
|
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role) ?: "SUPPORTING"
|
?: "MAIN"
|
||||||
|
|
||||||
|
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
|
||||||
|
?: "SUPPORTING"
|
||||||
|
|
||||||
else -> i.role.toString()
|
else -> i.role.toString()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -129,11 +141,16 @@ class AnilistQueries {
|
|||||||
val m = Media(mediaEdge)
|
val m = Media(mediaEdge)
|
||||||
media.relations?.add(m)
|
media.relations?.add(m)
|
||||||
if (m.relation == "SEQUEL") {
|
if (m.relation == "SEQUEL") {
|
||||||
media.sequel = if ((media.sequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.sequel
|
media.sequel =
|
||||||
|
if ((media.sequel?.popularity ?: 0) < (m.popularity
|
||||||
|
?: 0)
|
||||||
|
) m else media.sequel
|
||||||
|
|
||||||
} else if (m.relation == "PREQUEL") {
|
} else if (m.relation == "PREQUEL") {
|
||||||
media.prequel =
|
media.prequel =
|
||||||
if ((media.prequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.prequel
|
if ((media.prequel?.popularity ?: 0) < (m.popularity
|
||||||
|
?: 0)
|
||||||
|
) m else media.prequel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
media.relations?.sortByDescending { it.popularity }
|
media.relations?.sortByDescending { it.popularity }
|
||||||
@@ -199,17 +216,19 @@ class AnilistQueries {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
media.anime.nextAiringEpisodeTime = fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
media.anime.nextAiringEpisodeTime =
|
||||||
|
fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
||||||
|
|
||||||
fetchedMedia.externalLinks?.forEach { i ->
|
fetchedMedia.externalLinks?.forEach { i ->
|
||||||
when (i.site.lowercase()) {
|
when (i.site.lowercase()) {
|
||||||
"youtube" -> media.anime.youtube = i.url
|
"youtube" -> media.anime.youtube = i.url
|
||||||
"crunchyroll" -> media.crunchySlug = i.url?.split("/")?.getOrNull(3)
|
"crunchyroll" -> media.crunchySlug =
|
||||||
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
i.url?.split("/")?.getOrNull(3)
|
||||||
|
|
||||||
|
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (media.manga != null) {
|
||||||
else if (media.manga != null) {
|
|
||||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||||
media.manga.author = Author(
|
media.manga.author = Author(
|
||||||
it.id.toString(),
|
it.id.toString(),
|
||||||
@@ -228,7 +247,9 @@ class AnilistQueries {
|
|||||||
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
if (currContext()?.let { isOnline(it) } == true) {
|
||||||
|
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val mal = async {
|
val mal = async {
|
||||||
@@ -241,15 +262,36 @@ class AnilistQueries {
|
|||||||
return media
|
return media
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun continueMedia(type: String,planned:Boolean=false): ArrayList<Media> {
|
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
|
||||||
val returnArray = arrayListOf<Media>()
|
val returnArray = arrayListOf<Media>()
|
||||||
val map = mutableMapOf<Int, Media>()
|
val map = mutableMapOf<Int, Media>()
|
||||||
val statuses = if(!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING")
|
val query = if (planned) {
|
||||||
suspend fun repeat(status: String) {
|
"""{ planned: ${continueMediaQuery(type, "PLANNING")} }"""
|
||||||
val response =
|
} else {
|
||||||
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 } } } } } } """)
|
"""{
|
||||||
|
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 {
|
li.entries?.reversed()?.forEach {
|
||||||
val m = Media(it)
|
val m = Media(it)
|
||||||
m.cameFromContinue = true
|
m.cameFromContinue = true
|
||||||
@@ -257,10 +299,8 @@ class AnilistQueries {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||||
statuses.forEach { repeat(it) }
|
if (set.isNotEmpty()) {
|
||||||
val set = loadData<MutableSet<Int>>("continue_$type")
|
|
||||||
if (set != null) {
|
|
||||||
set.reversed().forEach {
|
set.reversed().forEach {
|
||||||
if (map.containsKey(it)) returnArray.add(map[it]!!)
|
if (map.containsKey(it)) returnArray.add(map[it]!!)
|
||||||
}
|
}
|
||||||
@@ -271,34 +311,40 @@ class AnilistQueries {
|
|||||||
return returnArray
|
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> {
|
suspend fun favMedia(anime: Boolean): ArrayList<Media> {
|
||||||
var hasNextPage = true
|
var hasNextPage = true
|
||||||
var page = 0
|
var page = 0
|
||||||
|
|
||||||
suspend fun getNextPage(page:Int): List<Media> {
|
suspend fun getNextPage(page: Int): List<Media> {
|
||||||
val response =
|
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page)}}""")
|
||||||
executeQuery<Query.User>("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""")
|
|
||||||
val favourites = response?.data?.user?.favourites
|
val favourites = response?.data?.user?.favourites
|
||||||
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
||||||
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
||||||
return apiMediaList?.edges?.mapNotNull {
|
return apiMediaList?.edges?.mapNotNull {
|
||||||
it.node?.let { i->
|
it.node?.let { i ->
|
||||||
Media(i).apply { isFav = true }
|
Media(i).apply { isFav = true }
|
||||||
}
|
}
|
||||||
} ?: return listOf()
|
} ?: return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
val responseArray = arrayListOf<Media>()
|
val responseArray = arrayListOf<Media>()
|
||||||
while(hasNextPage){
|
while (hasNextPage) {
|
||||||
page++
|
page++
|
||||||
responseArray.addAll(getNextPage(page))
|
responseArray.addAll(getNextPage(page))
|
||||||
}
|
}
|
||||||
return responseArray
|
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> {
|
suspend fun recommendations(): ArrayList<Media> {
|
||||||
val response =
|
val response = executeQuery<Query.Page>("""{${recommendationQuery()}}""")
|
||||||
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 map = mutableMapOf<Int, Media>()
|
val map = mutableMapOf<Int, Media>()
|
||||||
response?.data?.page?.apply {
|
response?.data?.page?.apply {
|
||||||
recommendations?.onEach {
|
recommendations?.onEach {
|
||||||
@@ -314,7 +360,7 @@ class AnilistQueries {
|
|||||||
val types = arrayOf("ANIME", "MANGA")
|
val types = arrayOf("ANIME", "MANGA")
|
||||||
suspend fun repeat(type: String) {
|
suspend fun repeat(type: String) {
|
||||||
val res =
|
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 ->
|
res?.data?.mediaListCollection?.lists?.forEach { li ->
|
||||||
li.entries?.forEach {
|
li.entries?.forEach {
|
||||||
val m = Media(it)
|
val m = Media(it)
|
||||||
@@ -332,8 +378,191 @@ class AnilistQueries {
|
|||||||
return list
|
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? {
|
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()) {
|
if (image == null || image.checkTime()) {
|
||||||
val response =
|
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 } } } } } """)
|
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
|
||||||
@@ -344,13 +573,9 @@ class AnilistQueries {
|
|||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
}?.flatten()?.randomOrNull() ?: return null
|
}?.flatten()?.randomOrNull() ?: return null
|
||||||
|
PrefManager.setCustomVal("banner_${type}_url", random)
|
||||||
image = BannerImage(
|
PrefManager.setCustomVal("banner_${type}_time", System.currentTimeMillis())
|
||||||
random,
|
return random
|
||||||
System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
saveData("banner_$type", image)
|
|
||||||
return image.url
|
|
||||||
} else return image.url
|
} else return image.url
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +586,11 @@ class AnilistQueries {
|
|||||||
return default
|
return default
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMediaLists(anime: Boolean, userId: Int, sortOrder: String? = null): MutableMap<String, ArrayList<Media>> {
|
suspend fun getMediaLists(
|
||||||
|
anime: Boolean,
|
||||||
|
userId: Int,
|
||||||
|
sortOrder: String? = null
|
||||||
|
): MutableMap<String, ArrayList<Media>> {
|
||||||
val response =
|
val response =
|
||||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
|
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
|
||||||
val sorted = mutableMapOf<String, ArrayList<Media>>()
|
val sorted = mutableMapOf<String, ArrayList<Media>>()
|
||||||
@@ -388,33 +617,55 @@ class AnilistQueries {
|
|||||||
if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!!
|
if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!!
|
||||||
}
|
}
|
||||||
unsorted.forEach {
|
unsorted.forEach {
|
||||||
if(!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
||||||
}
|
}
|
||||||
|
|
||||||
sorted["Favourites"] = favMedia(anime)
|
sorted["Favourites"] = favMedia(anime)
|
||||||
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
||||||
|
//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
|
sorted["All"] = all
|
||||||
|
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
||||||
val sort = sortOrder ?: options?.rowOrder
|
else PrefManager.getVal(PrefName.MangaListSortOrder)
|
||||||
|
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
||||||
for (i in sorted.keys) {
|
for (i in sorted.keys) {
|
||||||
when (sort) {
|
when (sort) {
|
||||||
"score" -> sorted[i]?.sortWith { b, a -> compareValuesBy(a, b, { it.userScore }, { it.meanScore }) }
|
"score" -> sorted[i]?.sortWith { b, a ->
|
||||||
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
compareValuesBy(
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
{ it.userScore },
|
||||||
|
{ it.meanScore })
|
||||||
|
}
|
||||||
|
|
||||||
|
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
||||||
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
|
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
|
||||||
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
||||||
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun getGenresAndTags(activity: Activity): Boolean {
|
suspend fun getGenresAndTags(): Boolean {
|
||||||
var genres: ArrayList<String>? = loadData("genres_list", activity)
|
var genres: ArrayList<String>? = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||||
var tags: Map<Boolean, List<String>>? = loadData("tags_map", activity)
|
.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>(
|
executeQuery<Query.GenreCollection>(
|
||||||
"""{GenreCollection}""",
|
"""{GenreCollection}""",
|
||||||
force = true,
|
force = true,
|
||||||
@@ -424,7 +675,7 @@ class AnilistQueries {
|
|||||||
forEach {
|
forEach {
|
||||||
genres?.add(it)
|
genres?.add(it)
|
||||||
}
|
}
|
||||||
saveData("genres_list", genres!!)
|
PrefManager.setVal(PrefName.GenresList, genres?.toSet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tags == null) {
|
if (tags == null) {
|
||||||
@@ -442,10 +693,11 @@ class AnilistQueries {
|
|||||||
true to adult,
|
true to adult,
|
||||||
false to good
|
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.genres = genres
|
||||||
Anilist.tags = tags
|
Anilist.tags = tags
|
||||||
true
|
true
|
||||||
@@ -462,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? {
|
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)) {
|
if (genres.checkGenreTime(genre)) {
|
||||||
try {
|
try {
|
||||||
val genreQuery =
|
val genreQuery =
|
||||||
@@ -476,7 +757,7 @@ class AnilistQueries {
|
|||||||
it.bannerImage!!,
|
it.bannerImage!!,
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
saveData("genre_thumb", genres)
|
saveSerializableMap("genre_thumb", genres)
|
||||||
return genres[genre]
|
return genres[genre]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,18 +840,36 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||||
${if (season != null) ""","season":"$season"""" else ""}
|
${if (season != null) ""","season":"$season"""" else ""}
|
||||||
${if (search != null) ""","search":"$search"""" else ""}
|
${if (search != null) ""","search":"$search"""" else ""}
|
||||||
${if (sort!=null) ""","sort":"$sort"""" else ""}
|
${if (sort != null) ""","sort":"$sort"""" else ""}
|
||||||
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
||||||
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
||||||
${
|
${
|
||||||
if (excludedGenres?.isNotEmpty() == true)
|
if (excludedGenres?.isNotEmpty() == true)
|
||||||
""","excludedGenres":[${excludedGenres.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
""","excludedGenres":[${
|
||||||
|
excludedGenres.joinToString {
|
||||||
|
"\"${
|
||||||
|
it.replace(
|
||||||
|
"Not ",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}\""
|
||||||
|
}
|
||||||
|
}]"""
|
||||||
else ""
|
else ""
|
||||||
}
|
}
|
||||||
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
|
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
|
||||||
${
|
${
|
||||||
if (excludedTags?.isNotEmpty() == true)
|
if (excludedTags?.isNotEmpty() == true)
|
||||||
""","excludedTags":[${excludedTags.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
""","excludedTags":[${
|
||||||
|
excludedTags.joinToString {
|
||||||
|
"\"${
|
||||||
|
it.replace(
|
||||||
|
"Not ",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}\""
|
||||||
|
}
|
||||||
|
}]"""
|
||||||
else ""
|
else ""
|
||||||
}
|
}
|
||||||
}""".replace("\n", " ").replace(""" """, "")
|
}""".replace("\n", " ").replace(""" """, "")
|
||||||
@@ -613,7 +912,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
page = pageInfo.currentPage.toString().toIntOrNull() ?: 0,
|
page = pageInfo.currentPage.toString().toIntOrNull() ?: 0,
|
||||||
hasNextPage = pageInfo.hasNextPage == true,
|
hasNextPage = pageInfo.hasNextPage == true,
|
||||||
)
|
)
|
||||||
} else snackString(currContext()?.getString(R.string.empty_response))
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,7 +921,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
greater: Long = 0,
|
greater: Long = 0,
|
||||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||||
): MutableList<Media>? {
|
): MutableList<Media>? {
|
||||||
suspend fun execute(page:Int = 1):Page?{
|
suspend fun execute(page: Int = 1): Page? {
|
||||||
val query = """{
|
val query = """{
|
||||||
Page(page:$page,perPage:50) {
|
Page(page:$page,perPage:50) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
@@ -668,10 +967,10 @@ Page(page:$page,perPage:50) {
|
|||||||
}""".replace("\n", " ").replace(""" """, "")
|
}""".replace("\n", " ").replace(""" """, "")
|
||||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||||
}
|
}
|
||||||
if(smaller) {
|
if (smaller) {
|
||||||
val response = execute()?.airingSchedules ?: return null
|
val response = execute()?.airingSchedules ?: return null
|
||||||
val idArr = mutableListOf<Int>()
|
val idArr = mutableListOf<Int>()
|
||||||
val listOnly = loadData("recently_list_only") ?: false
|
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||||
return response.mapNotNull { i ->
|
return response.mapNotNull { i ->
|
||||||
i.media?.let {
|
i.media?.let {
|
||||||
if (!idArr.contains(it.id))
|
if (!idArr.contains(it.id))
|
||||||
@@ -682,11 +981,11 @@ Page(page:$page,perPage:50) {
|
|||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
}else{
|
} else {
|
||||||
var i = 1
|
var i = 1
|
||||||
val list = mutableListOf<Media>()
|
val list = mutableListOf<Media>()
|
||||||
var res : Page? = null
|
var res: Page? = null
|
||||||
suspend fun next(){
|
suspend fun next() {
|
||||||
res = execute(i)
|
res = execute(i)
|
||||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
||||||
j.media?.let {
|
j.media?.let {
|
||||||
@@ -694,10 +993,10 @@ Page(page:$page,perPage:50) {
|
|||||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
}?: listOf())
|
} ?: listOf())
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
while (res?.pageInfo?.hasNextPage == true){
|
while (res?.pageInfo?.hasNextPage == true) {
|
||||||
next()
|
next()
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
@@ -822,19 +1121,20 @@ Page(page:$page,perPage:50) {
|
|||||||
var page = 0
|
var page = 0
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
page++
|
page++
|
||||||
hasNextPage = executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
hasNextPage =
|
||||||
it.edges?.forEach { i ->
|
executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
||||||
i.node?.apply {
|
it.edges?.forEach { i ->
|
||||||
val status = status.toString()
|
i.node?.apply {
|
||||||
val year = startDate?.year?.toString() ?: "TBA"
|
val status = status.toString()
|
||||||
val title = if (status != "CANCELLED") year else status
|
val year = startDate?.year?.toString() ?: "TBA"
|
||||||
if (!yearMedia.containsKey(title))
|
val title = if (status != "CANCELLED") year else status
|
||||||
yearMedia[title] = arrayListOf()
|
if (!yearMedia.containsKey(title))
|
||||||
yearMedia[title]?.add(Media(this))
|
yearMedia[title] = arrayListOf()
|
||||||
|
yearMedia[title]?.add(Media(this))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
it.pageInfo?.hasNextPage == true
|
||||||
it.pageInfo?.hasNextPage == true
|
} ?: false
|
||||||
} ?: false
|
|
||||||
}
|
}
|
||||||
if (yearMedia.contains("CANCELLED")) {
|
if (yearMedia.contains("CANCELLED")) {
|
||||||
val a = yearMedia["CANCELLED"]!!
|
val a = yearMedia["CANCELLED"]!!
|
||||||
@@ -896,7 +1196,10 @@ Page(page:$page,perPage:50) {
|
|||||||
|
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
page++
|
page++
|
||||||
hasNextPage = executeQuery<Query.Author>(query(page), force = true)?.data?.author?.staffMedia?.let {
|
hasNextPage = executeQuery<Query.Author>(
|
||||||
|
query(page),
|
||||||
|
force = true
|
||||||
|
)?.data?.author?.staffMedia?.let {
|
||||||
it.edges?.forEach { i ->
|
it.edges?.forEach { i ->
|
||||||
i.node?.apply {
|
i.node?.apply {
|
||||||
val status = status.toString()
|
val status = status.toString()
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import androidx.fragment.app.FragmentActivity
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import ani.dantotsu.BuildConfig
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.discord.Discord
|
import ani.dantotsu.connections.discord.Discord
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.others.AppUpdater
|
import ani.dantotsu.others.AppUpdater
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -19,9 +21,12 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
if (Discord.userid == null && Discord.token != null) {
|
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||||
if (!Discord.getUserData())
|
val userid = PrefManager.getVal(PrefName.DiscordId, null as String?)
|
||||||
snackString(context.getString(R.string.error_loading_discord_user_data))
|
if (userid == null && token != null) {
|
||||||
|
/*if (!Discord.getUserData())
|
||||||
|
snackString(context.getString(R.string.error_loading_discord_user_data))*/
|
||||||
|
//TODO: Discord.getUserData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,48 +43,79 @@ suspend fun getUserId(context: Context, block: () -> Unit) {
|
|||||||
}
|
}
|
||||||
} else true
|
} else true
|
||||||
|
|
||||||
if(anilist) block.invoke()
|
if (anilist) block.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnilistHomeViewModel : ViewModel() {
|
class AnilistHomeViewModel : ViewModel() {
|
||||||
private val listImages: MutableLiveData<ArrayList<String?>> = MutableLiveData<ArrayList<String?>>(arrayListOf())
|
private val listImages: MutableLiveData<ArrayList<String?>> =
|
||||||
|
MutableLiveData<ArrayList<String?>>(arrayListOf())
|
||||||
|
|
||||||
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
||||||
suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages())
|
suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages())
|
||||||
|
|
||||||
private val animeContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val animeContinue: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
||||||
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
||||||
|
|
||||||
private val animeFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
||||||
|
|
||||||
private val animePlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
||||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
suspend fun setAnimePlanned() = animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
|
||||||
|
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||||
|
suspend fun setAnimePlanned() =
|
||||||
|
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||||
|
|
||||||
|
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
|
||||||
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
||||||
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
||||||
|
|
||||||
private val mangaFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
||||||
|
|
||||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
||||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
suspend fun setMangaPlanned() = mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
|
||||||
|
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||||
|
suspend fun setMangaPlanned() =
|
||||||
|
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||||
|
|
||||||
|
private val recommendation: MutableLiveData<ArrayList<Media>> =
|
||||||
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
private val recommendation: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
|
||||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||||
|
|
||||||
|
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) {
|
suspend fun loadMain(context: FragmentActivity) {
|
||||||
Anilist.getSavedToken(context)
|
Anilist.getSavedToken()
|
||||||
MAL.getSavedToken(context)
|
MAL.getSavedToken(context)
|
||||||
Discord.getSavedToken(context)
|
Discord.getSavedToken(context)
|
||||||
if (loadData<Boolean>("check_update") != false) AppUpdater.check(context)
|
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||||
genres.postValue(Anilist.query.getGenresAndTags(context))
|
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||||
|
}
|
||||||
|
genres.postValue(Anilist.query.getGenresAndTags())
|
||||||
}
|
}
|
||||||
|
|
||||||
val empty = MutableLiveData<Boolean>(null)
|
val empty = MutableLiveData<Boolean>(null)
|
||||||
@@ -93,7 +129,9 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||||||
var notSet = true
|
var notSet = true
|
||||||
lateinit var searchResults: SearchResults
|
lateinit var searchResults: SearchResults
|
||||||
private val type = "ANIME"
|
private val type = "ANIME"
|
||||||
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
private val trending: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||||
suspend fun loadTrending(i: Int) {
|
suspend fun loadTrending(i: Int) {
|
||||||
val (season, year) = Anilist.currentSeasons[i]
|
val (season, year) = Anilist.currentSeasons[i]
|
||||||
@@ -109,7 +147,9 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
private val updated: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||||
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
||||||
|
|
||||||
@@ -157,15 +197,33 @@ class AnilistMangaViewModel : ViewModel() {
|
|||||||
var notSet = true
|
var notSet = true
|
||||||
lateinit var searchResults: SearchResults
|
lateinit var searchResults: SearchResults
|
||||||
private val type = "MANGA"
|
private val type = "MANGA"
|
||||||
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
private val trending: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||||
suspend fun loadTrending() =
|
suspend fun loadTrending() =
|
||||||
trending.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], hd = true)?.results)
|
trending.postValue(
|
||||||
|
Anilist.query.search(
|
||||||
|
type,
|
||||||
|
perPage = 10,
|
||||||
|
sort = Anilist.sortBy[2],
|
||||||
|
hd = true
|
||||||
|
)?.results
|
||||||
|
)
|
||||||
|
|
||||||
|
private val updated: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
|
||||||
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
|
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
|
||||||
suspend fun loadTrendingNovel() =
|
suspend fun loadTrendingNovel() =
|
||||||
updated.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], format = "NOVEL")?.results)
|
updated.postValue(
|
||||||
|
Anilist.query.search(
|
||||||
|
type,
|
||||||
|
perPage = 10,
|
||||||
|
sort = Anilist.sortBy[2],
|
||||||
|
format = "NOVEL"
|
||||||
|
)?.results
|
||||||
|
)
|
||||||
|
|
||||||
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
||||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
package ani.dantotsu.connections.anilist
|
package ani.dantotsu.connections.anilist
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.logger
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
val data: Uri? = intent?.data
|
val data: Uri? = intent?.data
|
||||||
logger(data.toString())
|
logger(data.toString())
|
||||||
try {
|
try {
|
||||||
Anilist.token = Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
Anilist.token =
|
||||||
val filename = "anilistToken"
|
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||||
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
|
PrefManager.setVal(PrefName.AnilistToken, Anilist.token ?: "")
|
||||||
it.write(Anilist.token!!.toByteArray())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,15 @@ data class SearchResults(
|
|||||||
val list = mutableListOf<SearchChip>()
|
val list = mutableListOf<SearchChip>()
|
||||||
sort?.let {
|
sort?.let {
|
||||||
val c = currContext()!!
|
val c = currContext()!!
|
||||||
list.add(SearchChip("SORT", c.getString(R.string.filter_sort, c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)])))
|
list.add(
|
||||||
|
SearchChip(
|
||||||
|
"SORT",
|
||||||
|
c.getString(
|
||||||
|
R.string.filter_sort,
|
||||||
|
c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
format?.let {
|
format?.let {
|
||||||
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
||||||
@@ -42,27 +50,37 @@ data class SearchResults(
|
|||||||
list.add(SearchChip("GENRE", it))
|
list.add(SearchChip("GENRE", it))
|
||||||
}
|
}
|
||||||
excludedGenres?.forEach {
|
excludedGenres?.forEach {
|
||||||
list.add(SearchChip("EXCLUDED_GENRE", currContext()!!.getString(R.string.filter_exclude, it)))
|
list.add(
|
||||||
|
SearchChip(
|
||||||
|
"EXCLUDED_GENRE",
|
||||||
|
currContext()!!.getString(R.string.filter_exclude, it)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
tags?.forEach {
|
tags?.forEach {
|
||||||
list.add(SearchChip("TAG", it))
|
list.add(SearchChip("TAG", it))
|
||||||
}
|
}
|
||||||
excludedTags?.forEach {
|
excludedTags?.forEach {
|
||||||
list.add(SearchChip("EXCLUDED_TAG", currContext()!!.getString(R.string.filter_exclude, it)))
|
list.add(
|
||||||
|
SearchChip(
|
||||||
|
"EXCLUDED_TAG",
|
||||||
|
currContext()!!.getString(R.string.filter_exclude, it)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeChip(chip: SearchChip) {
|
fun removeChip(chip: SearchChip) {
|
||||||
when (chip.type) {
|
when (chip.type) {
|
||||||
"SORT" -> sort = null
|
"SORT" -> sort = null
|
||||||
"FORMAT" -> format = null
|
"FORMAT" -> format = null
|
||||||
"SEASON" -> season = null
|
"SEASON" -> season = null
|
||||||
"SEASON_YEAR" -> seasonYear = null
|
"SEASON_YEAR" -> seasonYear = null
|
||||||
"GENRE" -> genres?.remove(chip.text)
|
"GENRE" -> genres?.remove(chip.text)
|
||||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||||
"TAG" -> tags?.remove(chip.text)
|
"TAG" -> tags?.remove(chip.text)
|
||||||
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import androidx.core.os.bundleOf
|
|||||||
import ani.dantotsu.loadMedia
|
import ani.dantotsu.loadMedia
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
|
|
||||||
class UrlMedia : Activity() {
|
class UrlMedia : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||||
var isMAL = false
|
var isMAL = false
|
||||||
var continueMedia = true
|
var continueMedia = true
|
||||||
@@ -23,6 +22,9 @@ ThemeManager(this).applyTheme()
|
|||||||
isMAL = data?.host != "anilist.co"
|
isMAL = data?.host != "anilist.co"
|
||||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||||
} else loadMedia = id
|
} else loadMedia = id
|
||||||
startMainActivity(this, bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia))
|
startMainActivity(
|
||||||
|
this,
|
||||||
|
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,23 +3,24 @@ package ani.dantotsu.connections.anilist.api
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
class Query{
|
class Query {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Viewer(
|
data class Viewer(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Viewer")
|
@SerialName("Viewer")
|
||||||
val user: ani.dantotsu.connections.anilist.api.User?
|
val user: ani.dantotsu.connections.anilist.api.User?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Media(
|
data class Media(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Media")
|
@SerialName("Media")
|
||||||
@@ -30,12 +31,12 @@ class Query{
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Page(
|
data class Page(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Page")
|
@SerialName("Page")
|
||||||
val page : ani.dantotsu.connections.anilist.api.Page?
|
val page: ani.dantotsu.connections.anilist.api.Page?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// data class AiringSchedule(
|
// data class AiringSchedule(
|
||||||
@@ -49,8 +50,8 @@ class Query{
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Character(
|
data class Character(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@@ -63,7 +64,7 @@ class Query{
|
|||||||
data class Studio(
|
data class Studio(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Studio")
|
@SerialName("Studio")
|
||||||
@@ -76,7 +77,7 @@ class Query{
|
|||||||
data class Author(
|
data class Author(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Staff")
|
@SerialName("Staff")
|
||||||
@@ -95,8 +96,8 @@ class Query{
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MediaListCollection(
|
data class MediaListCollection(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data : Data?
|
val data: Data?
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("MediaListCollection")
|
@SerialName("MediaListCollection")
|
||||||
@@ -104,11 +105,45 @@ 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
|
@Serializable
|
||||||
data class GenreCollection(
|
data class GenreCollection(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("GenreCollection")
|
@SerialName("GenreCollection")
|
||||||
@@ -120,7 +155,7 @@ class Query{
|
|||||||
data class MediaTagCollection(
|
data class MediaTagCollection(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("MediaTagCollection")
|
@SerialName("MediaTagCollection")
|
||||||
@@ -132,7 +167,7 @@ class Query{
|
|||||||
data class User(
|
data class User(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("User")
|
@SerialName("User")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package ani.dantotsu.connections.anilist.api
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.text.DateFormatSymbols
|
import java.text.DateFormatSymbols
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class FuzzyDate(
|
data class FuzzyDate(
|
||||||
@@ -16,9 +16,11 @@ data class FuzzyDate(
|
|||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
return year == null && month == null && day == null
|
return year == null && month == null && day == null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return if ( isEmpty() ) "??" else toStringOrEmpty()
|
return if (isEmpty()) "??" else toStringOrEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toStringOrEmpty(): String {
|
fun toStringOrEmpty(): String {
|
||||||
return listOfNotNull(
|
return listOfNotNull(
|
||||||
day?.toString(),
|
day?.toString(),
|
||||||
@@ -29,16 +31,21 @@ data class FuzzyDate(
|
|||||||
|
|
||||||
fun getToday(): FuzzyDate {
|
fun getToday(): FuzzyDate {
|
||||||
val cal = Calendar.getInstance()
|
val cal = Calendar.getInstance()
|
||||||
return FuzzyDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH))
|
return FuzzyDate(
|
||||||
|
cal.get(Calendar.YEAR),
|
||||||
|
cal.get(Calendar.MONTH) + 1,
|
||||||
|
cal.get(Calendar.DAY_OF_MONTH)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toVariableString(): String {
|
fun toVariableString(): String {
|
||||||
return listOfNotNull(
|
return listOfNotNull(
|
||||||
year?.let {"year:$it"},
|
year?.let { "year:$it" },
|
||||||
month?.let {"month:$it"},
|
month?.let { "month:$it" },
|
||||||
day?.let {"day:$it"}
|
day?.let { "day:$it" }
|
||||||
).joinToString(",", "{", "}")
|
).joinToString(",", "{", "}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toMALString(): String {
|
fun toMALString(): String {
|
||||||
val padding = '0'
|
val padding = '0'
|
||||||
val values = listOf(
|
val values = listOf(
|
||||||
@@ -46,7 +53,7 @@ data class FuzzyDate(
|
|||||||
month?.toString()?.padStart(2, padding),
|
month?.toString()?.padStart(2, padding),
|
||||||
day?.toString()?.padStart(2, padding)
|
day?.toString()?.padStart(2, padding)
|
||||||
)
|
)
|
||||||
return values.takeWhile {it is String}.joinToString("-")
|
return values.takeWhile { it is String }.joinToString("-")
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun toInt(): Int {
|
// fun toInt(): Int {
|
||||||
@@ -54,8 +61,8 @@ data class FuzzyDate(
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
override fun compareTo(other: FuzzyDate): Int = when {
|
override fun compareTo(other: FuzzyDate): Int = when {
|
||||||
year != other.year -> (year ?: 0) - (other.year ?: 0)
|
year != other.year -> (year ?: 0) - (other.year ?: 0)
|
||||||
month != other.month -> (month ?: 0) - (other.month ?: 0)
|
month != other.month -> (month ?: 0) - (other.month ?: 0)
|
||||||
else -> (day ?: 0) - (other.day ?: 0)
|
else -> (day ?: 0) - (other.day ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ data class Media(
|
|||||||
@SerialName("characters") var characters: CharacterConnection?,
|
@SerialName("characters") var characters: CharacterConnection?,
|
||||||
|
|
||||||
// The staff who produced the media
|
// The staff who produced the media
|
||||||
@SerialName("staffPreview") var staff: StaffConnection?,
|
@SerialName("staffPreview") var staff: StaffConnection?,
|
||||||
|
|
||||||
// The companies who produced the media
|
// The companies who produced the media
|
||||||
@SerialName("studios") var studios: StudioConnection?,
|
@SerialName("studios") var studios: StudioConnection?,
|
||||||
@@ -292,7 +292,7 @@ data class MediaList(
|
|||||||
@SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?,
|
@SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?,
|
||||||
|
|
||||||
// Map of booleans for which custom lists the entry are in
|
// Map of booleans for which custom lists the entry are in
|
||||||
@SerialName("customLists") var customLists: Map<String,Boolean>?,
|
@SerialName("customLists") var customLists: Map<String, Boolean>?,
|
||||||
|
|
||||||
// Map of advanced scores with name keys
|
// Map of advanced scores with name keys
|
||||||
// @SerialName("advancedScores") var advancedScores: Json?,
|
// @SerialName("advancedScores") var advancedScores: Json?,
|
||||||
@@ -355,7 +355,7 @@ data class MediaTrailer(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MediaTagCollection(
|
data class MediaTagCollection(
|
||||||
@SerialName("tags") var tags : List<MediaTag>?
|
@SerialName("tags") var tags: List<MediaTag>?
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ani.dantotsu.connections.anilist.api
|
|||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Recommendation(
|
data class Recommendation(
|
||||||
// The id of the recommendation
|
// The id of the recommendation
|
||||||
@@ -22,6 +23,7 @@ data class Recommendation(
|
|||||||
// The user that first created the recommendation
|
// The user that first created the recommendation
|
||||||
@SerialName("user") var user: User?,
|
@SerialName("user") var user: User?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RecommendationConnection(
|
data class RecommendationConnection(
|
||||||
//@SerialName("edges") var edges: List<RecommendationEdge>?,
|
//@SerialName("edges") var edges: List<RecommendationEdge>?,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ data class Staff(
|
|||||||
@SerialName("id") var id: Int,
|
@SerialName("id") var id: Int,
|
||||||
|
|
||||||
// The names of the staff member
|
// The names of the staff member
|
||||||
@SerialName("name") var name: StaffName?,
|
@SerialName("name") var name: StaffName?,
|
||||||
|
|
||||||
// The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan, Finnish, Turkish, Dutch, Swedish, Thai, Tagalog, Malaysian, Indonesian, Vietnamese, Nepali, Hindi, Urdu
|
// The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan, Finnish, Turkish, Dutch, Swedish, Thai, Tagalog, Malaysian, Indonesian, Vietnamese, Nepali, Hindi, Urdu
|
||||||
@SerialName("languageV2") var languageV2: String?,
|
@SerialName("languageV2") var languageV2: String?,
|
||||||
@@ -80,8 +80,8 @@ data class Staff(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StaffName (
|
data class StaffName(
|
||||||
var userPreferred:String?
|
var userPreferred: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -96,6 +96,6 @@ data class StaffConnection(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StaffEdge(
|
data class StaffEdge(
|
||||||
var role:String?,
|
var role: String?,
|
||||||
var node: Staff?
|
var node: Staff?
|
||||||
)
|
)
|
||||||
@@ -80,10 +80,10 @@ data class UserOptions(
|
|||||||
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
|
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
|
||||||
|
|
||||||
// Whether the user receives notifications when a show they are watching aires
|
// Whether the user receives notifications when a show they are watching aires
|
||||||
@SerialName("airingNotifications") var airingNotifications: Boolean?,
|
@SerialName("airingNotifications") var airingNotifications: Boolean?,
|
||||||
//
|
//
|
||||||
// Profile highlight color (blue, purple, pink, orange, red, green, gray)
|
// Profile highlight color (blue, purple, pink, orange, red, green, gray)
|
||||||
@SerialName("profileColor") var profileColor: String?,
|
@SerialName("profileColor") var profileColor: String?,
|
||||||
//
|
//
|
||||||
// // Notification options
|
// // Notification options
|
||||||
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
|
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
|
||||||
|
|||||||
@@ -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,16 +3,14 @@ package ani.dantotsu.connections.discord
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.edit
|
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.discord.serializers.User
|
|
||||||
import ani.dantotsu.others.CustomBottomDialog
|
import ani.dantotsu.others.CustomBottomDialog
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
import ani.dantotsu.tryWith
|
import ani.dantotsu.tryWith
|
||||||
import ani.dantotsu.tryWithSuspend
|
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Discord {
|
object Discord {
|
||||||
@@ -21,37 +19,20 @@ object Discord {
|
|||||||
var userid: String? = null
|
var userid: String? = null
|
||||||
var avatar: String? = null
|
var avatar: String? = null
|
||||||
|
|
||||||
private const val TOKEN = "discord_token"
|
|
||||||
|
|
||||||
fun getSavedToken(context: Context): Boolean {
|
fun getSavedToken(context: Context): Boolean {
|
||||||
val sharedPref = context.getSharedPreferences(
|
token = PrefManager.getVal(
|
||||||
context.getString(R.string.preference_file_key),
|
PrefName.DiscordToken, null as String?
|
||||||
Context.MODE_PRIVATE
|
|
||||||
)
|
)
|
||||||
token = sharedPref.getString(TOKEN, null)
|
|
||||||
return token != null
|
return token != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToken(context: Context, token: String) {
|
fun saveToken(context: Context, token: String) {
|
||||||
val sharedPref = context.getSharedPreferences(
|
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||||
context.getString(R.string.preference_file_key),
|
|
||||||
Context.MODE_PRIVATE
|
|
||||||
)
|
|
||||||
sharedPref.edit {
|
|
||||||
putString(TOKEN, token)
|
|
||||||
commit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSavedToken(context: Context) {
|
fun removeSavedToken(context: Context) {
|
||||||
val sharedPref = context.getSharedPreferences(
|
PrefManager.removeVal(PrefName.DiscordToken)
|
||||||
context.getString(R.string.preference_file_key),
|
|
||||||
Context.MODE_PRIVATE
|
|
||||||
)
|
|
||||||
sharedPref.edit {
|
|
||||||
remove(TOKEN)
|
|
||||||
commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
tryWith(true) {
|
tryWith(true) {
|
||||||
val dir = File(context.filesDir?.parentFile, "app_webview")
|
val dir = File(context.filesDir?.parentFile, "app_webview")
|
||||||
@@ -60,17 +41,7 @@ object Discord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var rpc : RPC? = null
|
private var rpc: RPC? = null
|
||||||
suspend fun getUserData() = tryWithSuspend(true) {
|
|
||||||
if(rpc==null) {
|
|
||||||
val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it }
|
|
||||||
val user: User = rpc.getUserData()
|
|
||||||
userid = user.username
|
|
||||||
avatar = user.userAvatar()
|
|
||||||
rpc.close()
|
|
||||||
true
|
|
||||||
} else true
|
|
||||||
} ?: false
|
|
||||||
|
|
||||||
|
|
||||||
fun warning(context: Context) = CustomBottomDialog().apply {
|
fun warning(context: Context) = CustomBottomDialog().apply {
|
||||||
@@ -97,16 +68,21 @@ object Discord {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultRPC(): RPC? {
|
const val application_Id = "1163925779692912771"
|
||||||
|
const val small_Image: String =
|
||||||
|
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
|
||||||
|
/*fun defaultRPC(): RPC? {
|
||||||
return token?.let {
|
return token?.let {
|
||||||
RPC(it, Dispatchers.IO).apply {
|
RPC(it, Dispatchers.IO).apply {
|
||||||
applicationId = "1163925779692912771"
|
applicationId = application_Id
|
||||||
smallImage = RPC.Link(
|
smallImage = RPC.Link(
|
||||||
"Dantotsu",
|
"Dantotsu",
|
||||||
"mp:attachments/1163940221063278672/1163940262423298141/bitmap1024.png"
|
small_Image
|
||||||
)
|
)
|
||||||
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
|
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
package ani.dantotsu.connections.discord
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import ani.dantotsu.MainActivity
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.connections.discord.serializers.Presence
|
||||||
|
import ani.dantotsu.connections.discord.serializers.User
|
||||||
|
import ani.dantotsu.isOnline
|
||||||
|
import 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
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.WebSocket
|
||||||
|
import okhttp3.WebSocketListener
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
|
||||||
|
class DiscordService : Service() {
|
||||||
|
private var heartbeat: Int = 0
|
||||||
|
private var sequence: Int? = null
|
||||||
|
private var sessionId: String = ""
|
||||||
|
private var resume = false
|
||||||
|
private lateinit var logFile: File
|
||||||
|
private lateinit var webSocket: WebSocket
|
||||||
|
private lateinit var heartbeatThread: Thread
|
||||||
|
private lateinit var client: OkHttpClient
|
||||||
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
var presenceStore = ""
|
||||||
|
val json = Json {
|
||||||
|
encodeDefaults = true
|
||||||
|
allowStructuredMapKeys = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
coerceInputValues = true
|
||||||
|
}
|
||||||
|
var log = ""
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
log("Service onCreate()")
|
||||||
|
val powerManager = baseContext.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
wakeLock = powerManager.newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
|
"discordRPC:backgroundPresence"
|
||||||
|
)
|
||||||
|
wakeLock.acquire()
|
||||||
|
log("WakeLock Acquired")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val serviceChannel = NotificationChannel(
|
||||||
|
"discordPresence",
|
||||||
|
"Discord Presence Service Channel",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.createNotificationChannel(serviceChannel)
|
||||||
|
}
|
||||||
|
val intent = Intent(this, MainActivity::class.java).apply {
|
||||||
|
action = Intent.ACTION_MAIN
|
||||||
|
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
val pendingIntent =
|
||||||
|
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val builder = NotificationCompat.Builder(this, "discordPresence")
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher_round)
|
||||||
|
.setContentTitle("Discord Presence")
|
||||||
|
.setContentText("Running in the background")
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
startForeground(1, builder.build())
|
||||||
|
log("Foreground service started, notification shown")
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
SERVICE_RUNNING = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
log("Service onStartCommand()")
|
||||||
|
if (intent != null) {
|
||||||
|
if (intent.hasExtra("presence")) {
|
||||||
|
log("Service onStartCommand() setPresence")
|
||||||
|
val lPresence = intent.getStringExtra("presence")
|
||||||
|
if (this::webSocket.isInitialized) webSocket.send(lPresence!!)
|
||||||
|
presenceStore = lPresence!!
|
||||||
|
} else {
|
||||||
|
log("Service onStartCommand() no presence")
|
||||||
|
DiscordServiceRunningSingleton.running = false
|
||||||
|
//kill the client
|
||||||
|
client = OkHttpClient()
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_REDELIVER_INTENT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
log("Service Destroyed")
|
||||||
|
if (DiscordServiceRunningSingleton.running) {
|
||||||
|
log("Accidental Service Destruction, restarting service")
|
||||||
|
val intent = Intent(baseContext, DiscordService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
baseContext.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
baseContext.startService(intent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this::webSocket.isInitialized)
|
||||||
|
setPresence(
|
||||||
|
json.encodeToString(
|
||||||
|
Presence.Response(
|
||||||
|
3,
|
||||||
|
Presence(status = "offline")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
|
SERVICE_RUNNING = false
|
||||||
|
client = OkHttpClient()
|
||||||
|
if (this::webSocket.isInitialized) webSocket.close(1000, "Closed by user")
|
||||||
|
super.onDestroy()
|
||||||
|
//saveLogToFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveProfile(response: String) {
|
||||||
|
val user = json.decodeFromString<User.Response>(response).d.user
|
||||||
|
log("User data: $user")
|
||||||
|
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
|
||||||
|
|
||||||
|
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||||
|
|
||||||
|
var retryAttempts = 0
|
||||||
|
val maxRetryAttempts = 10
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
super.onOpen(webSocket, response)
|
||||||
|
this@DiscordService.webSocket = webSocket
|
||||||
|
log("WebSocket: Opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
super.onMessage(webSocket, text)
|
||||||
|
val json = JsonParser.parseString(text).asJsonObject
|
||||||
|
log("WebSocket: Received op code ${json.get("op")}")
|
||||||
|
when (json.get("op").asInt) {
|
||||||
|
0 -> {
|
||||||
|
if (json.has("s")) {
|
||||||
|
log("WebSocket: Sequence ${json.get("s")} Received")
|
||||||
|
sequence = json.get("s").asInt
|
||||||
|
}
|
||||||
|
if (json.get("t").asString != "READY") return
|
||||||
|
saveProfile(text)
|
||||||
|
log(text)
|
||||||
|
sessionId = json.get("d").asJsonObject.get("session_id").asString
|
||||||
|
log("WebSocket: SessionID ${json.get("d").asJsonObject.get("session_id")} Received")
|
||||||
|
if (presenceStore.isNotEmpty()) setPresence(presenceStore)
|
||||||
|
sendBroadcast(Intent("ServiceToConnectButton"))
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
log("WebSocket: Received Heartbeat request, sending heartbeat")
|
||||||
|
heartbeatThread.interrupt()
|
||||||
|
heartbeatSend(webSocket, sequence)
|
||||||
|
heartbeatThread = Thread(HeartbeatRunnable())
|
||||||
|
heartbeatThread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
7 -> {
|
||||||
|
resume = true
|
||||||
|
log("WebSocket: Requested to Restart, restarting")
|
||||||
|
webSocket.close(1000, "Requested to Restart by the server")
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json")
|
||||||
|
.build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
9 -> {
|
||||||
|
log("WebSocket: Invalid Session, restarting")
|
||||||
|
webSocket.close(1000, "Invalid Session")
|
||||||
|
Thread.sleep(5000)
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json")
|
||||||
|
.build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
10 -> {
|
||||||
|
heartbeat = json.get("d").asJsonObject.get("heartbeat_interval").asInt
|
||||||
|
heartbeatThread = Thread(HeartbeatRunnable())
|
||||||
|
heartbeatThread.start()
|
||||||
|
if (resume) {
|
||||||
|
log("WebSocket: Resuming because server requested")
|
||||||
|
resume()
|
||||||
|
resume = false
|
||||||
|
} else {
|
||||||
|
identify(webSocket, baseContext)
|
||||||
|
log("WebSocket: Identified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
11 -> {
|
||||||
|
log("WebSocket: Heartbeat ACKed")
|
||||||
|
heartbeatThread = Thread(HeartbeatRunnable())
|
||||||
|
heartbeatThread.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun identify(webSocket: WebSocket, context: Context) {
|
||||||
|
val properties = JsonObject()
|
||||||
|
properties.addProperty("os", "linux")
|
||||||
|
properties.addProperty("browser", "unknown")
|
||||||
|
properties.addProperty("device", "unknown")
|
||||||
|
val d = JsonObject()
|
||||||
|
d.addProperty("token", getToken(context))
|
||||||
|
d.addProperty("intents", 0)
|
||||||
|
d.add("properties", properties)
|
||||||
|
val payload = JsonObject()
|
||||||
|
payload.addProperty("op", 2)
|
||||||
|
payload.add("d", d)
|
||||||
|
webSocket.send(payload.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
|
super.onFailure(webSocket, t, response)
|
||||||
|
if (!isOnline(baseContext)) {
|
||||||
|
log("WebSocket: Error, onFailure() reason: No Internet")
|
||||||
|
errorNotification("Could not set the presence", "No Internet")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
retryAttempts++
|
||||||
|
if (retryAttempts >= maxRetryAttempts) {
|
||||||
|
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
|
||||||
|
errorNotification("Could not set the presence", "Max Retry Attempts")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
|
||||||
|
log("WebSocket: Error, onFailure() reason: ${t.message}")
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||||
|
heartbeatThread.interrupt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
super.onClosing(webSocket, code, reason)
|
||||||
|
Log.d("WebSocket", "onClosing() $code $reason")
|
||||||
|
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||||
|
heartbeatThread.interrupt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
super.onClosed(webSocket, code, reason)
|
||||||
|
Log.d("WebSocket", "onClosed() $code $reason")
|
||||||
|
if (code >= 4000) {
|
||||||
|
log("WebSocket: Error, code: $code reason: $reason")
|
||||||
|
client = OkHttpClient()
|
||||||
|
client.newWebSocket(
|
||||||
|
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
|
||||||
|
DiscordWebSocketListener()
|
||||||
|
)
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getToken(context: Context): String {
|
||||||
|
val 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")
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun heartbeatSend(webSocket: WebSocket, seq: Int?) {
|
||||||
|
val json = JsonObject()
|
||||||
|
json.addProperty("op", 1)
|
||||||
|
json.addProperty("d", seq)
|
||||||
|
webSocket.send(json.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun errorNotification(title: String, text: String) {
|
||||||
|
val intent = Intent(this@DiscordService, MainActivity::class.java).apply {
|
||||||
|
action = Intent.ACTION_MAIN
|
||||||
|
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
val pendingIntent =
|
||||||
|
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val builder = NotificationCompat.Builder(this@DiscordService, "discordPresence")
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher_round)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
//TODO: Request permission
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notificationManager.notify(2, builder.build())
|
||||||
|
log("Error Notified")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveSimpleTestPresence() {
|
||||||
|
val file = File(baseContext.cacheDir, "payload")
|
||||||
|
//fill with test payload
|
||||||
|
val payload = JsonObject()
|
||||||
|
payload.addProperty("op", 3)
|
||||||
|
payload.add("d", JsonObject().apply {
|
||||||
|
addProperty("status", "online")
|
||||||
|
addProperty("afk", false)
|
||||||
|
add("activities", JsonArray().apply {
|
||||||
|
add(JsonObject().apply {
|
||||||
|
addProperty("name", "Test")
|
||||||
|
addProperty("type", 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
file.writeText(payload.toString())
|
||||||
|
log("WebSocket: Simple Test Presence Saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPresence(String: String) {
|
||||||
|
log("WebSocket: Sending Presence payload")
|
||||||
|
log(String)
|
||||||
|
webSocket.send(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun log(string: String) {
|
||||||
|
Log.d("WebSocket_Discord", string)
|
||||||
|
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLogToFile() {
|
||||||
|
val fileName = "log_${System.currentTimeMillis()}.txt"
|
||||||
|
|
||||||
|
// ContentValues to store file metadata
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserting the file in the MediaStore
|
||||||
|
val resolver = baseContext.contentResolver
|
||||||
|
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||||
|
} else {
|
||||||
|
val directory =
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
val file = File(directory, fileName)
|
||||||
|
|
||||||
|
// Make sure the Downloads directory exists
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use FileProvider to get the URI for the file
|
||||||
|
val authority =
|
||||||
|
"${baseContext.packageName}.provider" // Adjust with your app's package name
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the file
|
||||||
|
uri?.let {
|
||||||
|
resolver.openOutputStream(it).use { outputStream ->
|
||||||
|
OutputStreamWriter(outputStream).use { writer ->
|
||||||
|
writer.write(log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
log("Error saving log file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resume() {
|
||||||
|
log("Sending Resume payload")
|
||||||
|
val d = JsonObject()
|
||||||
|
d.addProperty("token", getToken(baseContext))
|
||||||
|
d.addProperty("session_id", sessionId)
|
||||||
|
d.addProperty("seq", sequence)
|
||||||
|
val json = JsonObject()
|
||||||
|
json.addProperty("op", 6)
|
||||||
|
json.add("d", d)
|
||||||
|
log(json.toString())
|
||||||
|
webSocket.send(json.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class HeartbeatRunnable : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(heartbeat.toLong())
|
||||||
|
heartbeatSend(webSocket, sequence)
|
||||||
|
log("WebSocket: Heartbeat Sent")
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var SERVICE_RUNNING = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object DiscordServiceRunningSingleton {
|
||||||
|
var running = false
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,23 +4,23 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Application.getProcessName
|
import android.app.Application.getProcessName
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.annotation.RequiresApi
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
val process = getProcessName()
|
val process = getProcessName()
|
||||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||||
@@ -34,28 +34,46 @@ ThemeManager(this).applyTheme()
|
|||||||
settings.databaseEnabled = true
|
settings.databaseEnabled = true
|
||||||
settings.domStorageEnabled = true
|
settings.domStorageEnabled = true
|
||||||
}
|
}
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true)
|
||||||
webView.webViewClient = object : WebViewClient() {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
override fun shouldOverrideUrlLoading(
|
||||||
if (url != null && url.endsWith("/app")) {
|
view: WebView?,
|
||||||
webView.stopLoading()
|
request: WebResourceRequest?
|
||||||
webView.evaluateJavascript("""
|
): Boolean {
|
||||||
(function() {
|
// Check if the URL is the one expected after a successful login
|
||||||
const wreq = webpackChunkdiscord_app.push([[Symbol()], {}, w => w])
|
if (request?.url.toString() != "https://discord.com/login") {
|
||||||
webpackChunkdiscord_app.pop()
|
// Delay the script execution to ensure the page is fully loaded
|
||||||
const token = Object.values(wreq.c).find(m => m.exports?.Z?.getToken).exports.Z.getToken();
|
view?.postDelayed({
|
||||||
return token;
|
view.evaluateJavascript(
|
||||||
})()
|
"""
|
||||||
""".trimIndent()){
|
(function() {
|
||||||
login(it.trim('"'))
|
const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken();
|
||||||
}
|
return wreq;
|
||||||
|
})()
|
||||||
|
""".trimIndent()
|
||||||
|
) { result ->
|
||||||
|
login(result.trim('"'))
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
return super.shouldOverrideUrlLoading(view, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webView.loadUrl("https://discord.com/login")
|
webView.loadUrl("https://discord.com/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun login(token: String) {
|
private fun login(token: String) {
|
||||||
|
if (token.isEmpty() || token == "null") {
|
||||||
|
Toast.makeText(this, "Failed to retrieve token", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
||||||
finish()
|
finish()
|
||||||
saveToken(this, token)
|
saveToken(this, token)
|
||||||
startMainActivity(this@Login)
|
startMainActivity(this@Login)
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
package ani.dantotsu.connections.discord
|
package ani.dantotsu.connections.discord
|
||||||
|
|
||||||
import ani.dantotsu.connections.discord.serializers.*
|
import ani.dantotsu.connections.discord.serializers.Activity
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import ani.dantotsu.connections.discord.serializers.Presence
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.WebSocket
|
|
||||||
import okhttp3.WebSocketListener
|
|
||||||
import java.util.concurrent.TimeUnit.*
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import ani.dantotsu.client as app
|
import ani.dantotsu.client as app
|
||||||
|
|
||||||
@@ -31,205 +17,73 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
|||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(10, SECONDS)
|
|
||||||
.readTimeout(10, SECONDS)
|
|
||||||
.writeTimeout(10, SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val request = Request.Builder()
|
|
||||||
.url("wss://gateway.discord.gg/?encoding=json&v=10")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private var webSocket = client.newWebSocket(request, Listener())
|
|
||||||
|
|
||||||
var applicationId: String? = null
|
|
||||||
var type: Type? = null
|
|
||||||
var activityName: String? = null
|
|
||||||
var details: String? = null
|
|
||||||
var state: String? = null
|
|
||||||
var largeImage: Link? = null
|
|
||||||
var smallImage: Link? = null
|
|
||||||
var status: String? = null
|
|
||||||
var startTimestamp: Long? = null
|
|
||||||
var stopTimestamp: Long? = null
|
|
||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
|
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
|
||||||
}
|
}
|
||||||
|
|
||||||
var buttons = mutableListOf<Link>()
|
|
||||||
|
|
||||||
data class Link(val label: String, val url: String)
|
data class Link(val label: String, val url: String)
|
||||||
|
|
||||||
private suspend fun createPresence(): String {
|
companion object {
|
||||||
return json.encodeToString(Presence.Response(
|
data class RPCData(
|
||||||
3,
|
val applicationId: String? = null,
|
||||||
Presence(
|
val type: Type? = null,
|
||||||
activities = listOf(
|
val activityName: String? = null,
|
||||||
Activity(
|
val details: String? = null,
|
||||||
name = activityName,
|
val state: String? = null,
|
||||||
state = state,
|
val largeImage: Link? = null,
|
||||||
details = details,
|
val smallImage: Link? = null,
|
||||||
type = type?.ordinal,
|
val status: String? = null,
|
||||||
timestamps = if (startTimestamp != null)
|
val startTimestamp: Long? = null,
|
||||||
Activity.Timestamps(startTimestamp, stopTimestamp)
|
val stopTimestamp: Long? = null,
|
||||||
else null,
|
val buttons: MutableList<Link> = mutableListOf()
|
||||||
assets = Activity.Assets(
|
|
||||||
largeImage = largeImage?.url?.discordUrl(),
|
|
||||||
largeText = largeImage?.label,
|
|
||||||
smallImage = smallImage?.url?.discordUrl(),
|
|
||||||
smallText = smallImage?.label
|
|
||||||
),
|
|
||||||
buttons = buttons.map { it.label },
|
|
||||||
metadata = Activity.Metadata(
|
|
||||||
buttonUrls = buttons.map { it.url }
|
|
||||||
),
|
|
||||||
applicationId = applicationId,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
afk = true,
|
|
||||||
since = startTimestamp,
|
|
||||||
status = status
|
|
||||||
)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class KizzyApi(val id: String)
|
|
||||||
val api = "https://kizzy-api.vercel.app/image?url="
|
|
||||||
private suspend fun String.discordUrl(): String? {
|
|
||||||
if (startsWith("mp:")) return this
|
|
||||||
val json = app.get("$api$this").parsedSafe<KizzyApi>()
|
|
||||||
return json?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendIdentify() {
|
|
||||||
val response = Identity.Response(
|
|
||||||
op = 2,
|
|
||||||
d = Identity(
|
|
||||||
token = token,
|
|
||||||
properties = Identity.Properties(
|
|
||||||
os = "windows",
|
|
||||||
browser = "Chrome",
|
|
||||||
device = "disco"
|
|
||||||
),
|
|
||||||
compress = false,
|
|
||||||
intents = 0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
webSocket.send(json.encodeToString(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun send(block: RPC.() -> Unit) {
|
@Serializable
|
||||||
block.invoke(this)
|
data class KizzyApi(val id: String)
|
||||||
send()
|
|
||||||
}
|
|
||||||
|
|
||||||
var started = false
|
val api = "https://kizzy-api.vercel.app/image?url="
|
||||||
var whenStarted: ((User) -> Unit)? = null
|
private suspend fun String.discordUrl(): String? {
|
||||||
|
if (startsWith("mp:")) return this
|
||||||
|
val json = app.get("$api$this").parsedSafe<KizzyApi>()
|
||||||
|
return json?.id
|
||||||
|
}
|
||||||
|
|
||||||
fun send() {
|
suspend fun createPresence(data: RPCData): String {
|
||||||
val send = {
|
val json = Json {
|
||||||
CoroutineScope(coroutineContext).launch {
|
encodeDefaults = true
|
||||||
webSocket.send(createPresence())
|
allowStructuredMapKeys = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
}
|
return json.encodeToString(Presence.Response(
|
||||||
if (!started) whenStarted = {
|
3,
|
||||||
send.invoke()
|
Presence(
|
||||||
whenStarted = null
|
activities = listOf(
|
||||||
}
|
Activity(
|
||||||
else send.invoke()
|
name = data.activityName,
|
||||||
}
|
state = data.state,
|
||||||
|
details = data.details,
|
||||||
fun close() {
|
type = data.type?.ordinal,
|
||||||
webSocket.send(
|
timestamps = if (data.startTimestamp != null)
|
||||||
json.encodeToString(
|
Activity.Timestamps(data.startTimestamp, data.stopTimestamp)
|
||||||
Presence.Response(
|
else null,
|
||||||
3,
|
assets = Activity.Assets(
|
||||||
Presence(status = "offline")
|
largeImage = data.largeImage?.url?.discordUrl(),
|
||||||
|
largeText = data.largeImage?.label,
|
||||||
|
smallImage = data.smallImage?.url?.discordUrl(),
|
||||||
|
smallText = data.smallImage?.label
|
||||||
|
),
|
||||||
|
buttons = data.buttons.map { it.label },
|
||||||
|
metadata = Activity.Metadata(
|
||||||
|
buttonUrls = data.buttons.map { it.url }
|
||||||
|
),
|
||||||
|
applicationId = data.applicationId,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
afk = true,
|
||||||
|
since = data.startTimestamp,
|
||||||
|
status = data.status
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
)
|
|
||||||
webSocket.close(4000, "Interrupt")
|
|
||||||
}
|
|
||||||
|
|
||||||
//I hate this, but couldn't find any better way to solve it
|
|
||||||
suspend fun getUserData(): User {
|
|
||||||
var user : User? = null
|
|
||||||
whenStarted = {
|
|
||||||
user = it
|
|
||||||
whenStarted = null
|
|
||||||
}
|
|
||||||
while (user == null) {
|
|
||||||
delay(100)
|
|
||||||
}
|
|
||||||
return user!!
|
|
||||||
}
|
|
||||||
|
|
||||||
var onReceiveUserData: ((User) -> Deferred<Unit>)? = null
|
|
||||||
|
|
||||||
inner class Listener : WebSocketListener() {
|
|
||||||
private var seq: Int? = null
|
|
||||||
private var heartbeatInterval: Long? = null
|
|
||||||
|
|
||||||
var scope = CoroutineScope(coroutineContext)
|
|
||||||
|
|
||||||
private fun sendHeartBeat() {
|
|
||||||
scope.cancel()
|
|
||||||
scope = CoroutineScope(coroutineContext)
|
|
||||||
scope.launch {
|
|
||||||
delay(heartbeatInterval!!)
|
|
||||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
|
||||||
println("Message : $text")
|
|
||||||
|
|
||||||
val map = json.decodeFromString<Res>(text)
|
|
||||||
seq = map.s
|
|
||||||
|
|
||||||
when (map.op) {
|
|
||||||
10 -> {
|
|
||||||
map.d as JsonObject
|
|
||||||
heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long
|
|
||||||
sendHeartBeat()
|
|
||||||
sendIdentify()
|
|
||||||
}
|
|
||||||
|
|
||||||
0 -> if (map.t == "READY") {
|
|
||||||
val user = json.decodeFromString<User.Response>(text).d.user
|
|
||||||
started = true
|
|
||||||
whenStarted?.invoke(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
if (scope.isActive) scope.cancel()
|
|
||||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
|
||||||
}
|
|
||||||
|
|
||||||
11 -> sendHeartBeat()
|
|
||||||
7 -> webSocket.close(400, "Reconnect")
|
|
||||||
9 -> {
|
|
||||||
sendHeartBeat()
|
|
||||||
sendIdentify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
|
||||||
println("Server Closed : $code $reason")
|
|
||||||
if (code == 4000) {
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
|
||||||
println("Failure : ${t.message}")
|
|
||||||
if (t.message != "Interrupt") {
|
|
||||||
this@RPC.webSocket = client.newWebSocket(request, Listener())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package ani.dantotsu.connections.discord.serializers
|
|||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Activity (
|
data class Activity(
|
||||||
@SerialName("application_id")
|
@SerialName("application_id")
|
||||||
val applicationId: String? = null,
|
val applicationId: String? = null,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ data class Identity(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Response (
|
data class Response(
|
||||||
val op: Long,
|
val op: Long,
|
||||||
val d: Identity
|
val d: Identity
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Properties (
|
data class Properties(
|
||||||
@SerialName("\$os")
|
@SerialName("\$os")
|
||||||
val os: String,
|
val os: String,
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package ani.dantotsu.connections.discord.serializers
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Presence (
|
data class Presence(
|
||||||
val activities: List<Activity> = listOf(),
|
val activities: List<Activity> = listOf(),
|
||||||
val afk: Boolean = true,
|
val afk: Boolean = true,
|
||||||
val since: Long? = null,
|
val since: Long? = null,
|
||||||
val status: String? = null
|
val status: String? = null
|
||||||
){
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Response (
|
data class Response(
|
||||||
val op: Long,
|
val op: Long,
|
||||||
val d: Presence
|
val d: Presence
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
package ani.dantotsu.connections.discord.serializers
|
package ani.dantotsu.connections.discord.serializers
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.descriptors.*
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encoding.*
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.*
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class User (
|
data class User(
|
||||||
val verified: Boolean,
|
val verified: Boolean? = null,
|
||||||
val username: String,
|
val username: String,
|
||||||
|
|
||||||
@SerialName("purchased_flags")
|
@SerialName("purchased_flags")
|
||||||
val purchasedFlags: Long,
|
val purchasedFlags: Long? = null,
|
||||||
|
|
||||||
@SerialName("public_flags")
|
@SerialName("public_flags")
|
||||||
val publicFlags: Long,
|
val publicFlags: Long? = null,
|
||||||
|
|
||||||
val pronouns: String,
|
val pronouns: String? = null,
|
||||||
|
|
||||||
@SerialName("premium_type")
|
@SerialName("premium_type")
|
||||||
val premiumType: Long,
|
val premiumType: Long? = null,
|
||||||
|
|
||||||
val premium: Boolean,
|
val premium: Boolean? = null,
|
||||||
val phone: String,
|
val phone: String? = null,
|
||||||
|
|
||||||
@SerialName("nsfw_allowed")
|
@SerialName("nsfw_allowed")
|
||||||
val nsfwAllowed: Boolean,
|
val nsfwAllowed: Boolean? = null,
|
||||||
|
|
||||||
val mobile: Boolean,
|
val mobile: Boolean? = null,
|
||||||
|
|
||||||
@SerialName("mfa_enabled")
|
@SerialName("mfa_enabled")
|
||||||
val mfaEnabled: Boolean,
|
val mfaEnabled: Boolean? = null,
|
||||||
|
|
||||||
val id: String,
|
val id: String,
|
||||||
|
|
||||||
@SerialName("global_name")
|
@SerialName("global_name")
|
||||||
val globalName: String,
|
val globalName: String? = null,
|
||||||
|
|
||||||
val flags: Long,
|
val flags: Long? = null,
|
||||||
val email: String,
|
val email: String? = null,
|
||||||
val discriminator: String,
|
val discriminator: String? = null,
|
||||||
val desktop: Boolean,
|
val desktop: Boolean? = null,
|
||||||
val bio: String,
|
val bio: String? = null,
|
||||||
|
|
||||||
@SerialName("banner_color")
|
@SerialName("banner_color")
|
||||||
val bannerColor: String,
|
val bannerColor: String? = null,
|
||||||
|
|
||||||
val banner: JsonElement? = null,
|
val banner: JsonElement? = null,
|
||||||
|
|
||||||
@SerialName("avatar_decoration")
|
@SerialName("avatar_decoration")
|
||||||
val avatarDecoration: JsonElement? = null,
|
val avatarDecoration: JsonElement? = null,
|
||||||
|
|
||||||
val avatar: String,
|
val avatar: String? = null,
|
||||||
|
|
||||||
@SerialName("accent_color")
|
@SerialName("accent_color")
|
||||||
val accentColor: Long
|
val accentColor: Long? = null
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Response(
|
data class Response(
|
||||||
@@ -70,7 +70,7 @@ data class User (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun userAvatar():String{
|
fun userAvatar(): String {
|
||||||
return "https://cdn.discordapp.com/avatars/$id/$avatar.png"
|
return "https://cdn.discordapp.com/avatars/$id/$avatar.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,23 +4,29 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.client
|
||||||
import ani.dantotsu.connections.mal.MAL.clientId
|
import ani.dantotsu.connections.mal.MAL.clientId
|
||||||
import ani.dantotsu.connections.mal.MAL.saveResponse
|
import ani.dantotsu.connections.mal.MAL.saveResponse
|
||||||
|
import ani.dantotsu.logError
|
||||||
|
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
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
import ani.dantotsu.tryWithSuspend
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
try {
|
try {
|
||||||
val data: Uri = intent?.data
|
val data: Uri = intent?.data
|
||||||
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
||||||
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))
|
?: throw Exception(getString(R.string.mal_login_code_challenge_not_found))
|
||||||
val code = data.getQueryParameter("code")
|
val code = data.getQueryParameter("code")
|
||||||
?: throw Exception(getString(R.string.mal_login_code_not_present))
|
?: throw Exception(getString(R.string.mal_login_code_not_present))
|
||||||
@@ -46,9 +52,8 @@ ThemeManager(this).applyTheme()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
catch (e:Exception){
|
logError(e, snackbar = false)
|
||||||
logError(e,snackbar = false)
|
|
||||||
startMainActivity(this)
|
startMainActivity(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ import android.net.Uri
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.openLinkInBrowser
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.tryWithSuspend
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.io.File
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
object MAL {
|
object MAL {
|
||||||
@@ -28,7 +33,7 @@ object MAL {
|
|||||||
.replace("/", "_")
|
.replace("/", "_")
|
||||||
.replace("\n", "")
|
.replace("\n", "")
|
||||||
|
|
||||||
saveData("malCodeChallenge", codeChallenge, context)
|
PrefManager.setVal(PrefName.MALCodeChallenge, codeChallenge)
|
||||||
val request =
|
val request =
|
||||||
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
|
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
|
||||||
try {
|
try {
|
||||||
@@ -41,11 +46,9 @@ object MAL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val MAL_TOKEN = "malToken"
|
|
||||||
|
|
||||||
private suspend fun refreshToken(): ResponseToken? {
|
private suspend fun refreshToken(): ResponseToken? {
|
||||||
return tryWithSuspend {
|
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))
|
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
|
||||||
val res = client.post(
|
val res = client.post(
|
||||||
"https://myanimelist.net/v1/oauth2/token",
|
"https://myanimelist.net/v1/oauth2/token",
|
||||||
@@ -63,8 +66,9 @@ object MAL {
|
|||||||
|
|
||||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
||||||
return tryWithSuspend(false) {
|
return tryWithSuspend(false) {
|
||||||
var res: ResponseToken = loadData(MAL_TOKEN, context)
|
var res: ResponseToken =
|
||||||
?: return@tryWithSuspend false
|
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||||
|
?: return@tryWithSuspend false
|
||||||
if (System.currentTimeMillis() > res.expiresIn)
|
if (System.currentTimeMillis() > res.expiresIn)
|
||||||
res = refreshToken()
|
res = refreshToken()
|
||||||
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
|
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
|
||||||
@@ -78,14 +82,12 @@ object MAL {
|
|||||||
username = null
|
username = null
|
||||||
userid = null
|
userid = null
|
||||||
avatar = null
|
avatar = null
|
||||||
if (MAL_TOKEN in context.fileList()) {
|
PrefManager.removeVal(PrefName.MALToken)
|
||||||
File(context.filesDir, MAL_TOKEN).delete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveResponse(res: ResponseToken) {
|
fun saveResponse(res: ResponseToken) {
|
||||||
res.expiresIn += System.currentTimeMillis()
|
res.expiresIn += System.currentTimeMillis()
|
||||||
saveData(MAL_TOKEN, res)
|
PrefManager.setVal(PrefName.MALToken, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -94,6 +96,10 @@ object MAL {
|
|||||||
@SerialName("expires_in") var expiresIn: Long,
|
@SerialName("expires_in") var expiresIn: Long,
|
||||||
@SerialName("access_token") val accessToken: String,
|
@SerialName("access_token") val accessToken: String,
|
||||||
@SerialName("refresh_token") val refreshToken: String,
|
@SerialName("refresh_token") val refreshToken: String,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable {
|
||||||
|
companion object {
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package ani.dantotsu.connections.mal
|
package ani.dantotsu.connections.mal
|
||||||
|
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
|
||||||
import ani.dantotsu.client
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -43,18 +43,18 @@ class MALQueries {
|
|||||||
start: FuzzyDate? = null,
|
start: FuzzyDate? = null,
|
||||||
end: FuzzyDate? = null
|
end: FuzzyDate? = null
|
||||||
) {
|
) {
|
||||||
if(idMAL==null) return
|
if (idMAL == null) return
|
||||||
val data = mutableMapOf("status" to convertStatus(isAnime, status))
|
val data = mutableMapOf("status" to convertStatus(isAnime, status))
|
||||||
if (progress != null)
|
if (progress != null)
|
||||||
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
|
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
|
||||||
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
|
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
|
||||||
if (score != null)
|
if (score != null)
|
||||||
data["score"] = score.div(10).toString()
|
data["score"] = score.div(10).toString()
|
||||||
if(rewatch!=null)
|
if (rewatch != null)
|
||||||
data[if(isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
data[if (isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
||||||
if(start!=null)
|
if (start != null)
|
||||||
data["start_date"] = start.toMALString()
|
data["start_date"] = start.toMALString()
|
||||||
if(end!=null)
|
if (end != null)
|
||||||
data["finish_date"] = end.toMALString()
|
data["finish_date"] = end.toMALString()
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
client.put(
|
client.put(
|
||||||
@@ -65,8 +65,8 @@ class MALQueries {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteList(isAnime: Boolean, idMAL: Int?){
|
suspend fun deleteList(isAnime: Boolean, idMAL: Int?) {
|
||||||
if(idMAL==null) return
|
if (idMAL == null) return
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
client.delete(
|
client.delete(
|
||||||
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
|
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
|
||||||
|
|||||||
@@ -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.themes.ThemeManager
|
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
|
|
||||||
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,55 +1,182 @@
|
|||||||
package ani.dantotsu.download
|
package ani.dantotsu.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.widget.Toast
|
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.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
class DownloadsManager(private val context: Context) {
|
class DownloadsManager(private val context: Context) {
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
|
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private val downloadsList = loadDownloads().toMutableList()
|
private val downloadsList = loadDownloads().toMutableList()
|
||||||
|
|
||||||
val mangaDownloads: List<Download>
|
val mangaDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == Download.Type.MANGA }
|
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
||||||
val animeDownloads: List<Download>
|
val animeDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == Download.Type.ANIME }
|
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
||||||
|
val novelDownloadedTypes: List<DownloadedType>
|
||||||
|
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
||||||
|
|
||||||
private fun saveDownloads() {
|
private fun saveDownloads() {
|
||||||
val jsonString = gson.toJson(downloadsList)
|
val jsonString = gson.toJson(downloadsList)
|
||||||
prefs.edit().putString("downloads_key", jsonString).apply()
|
PrefManager.setVal(PrefName.DownloadsKeys, jsonString)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDownloads(): List<Download> {
|
private fun loadDownloads(): List<DownloadedType> {
|
||||||
val jsonString = prefs.getString("downloads_key", null)
|
val jsonString = PrefManager.getVal(PrefName.DownloadsKeys, null as String?)
|
||||||
return if (jsonString != null) {
|
return if (jsonString != null) {
|
||||||
val type = object : TypeToken<List<Download>>() {}.type
|
val type = object : TypeToken<List<DownloadedType>>() {}.type
|
||||||
gson.fromJson(jsonString, type)
|
gson.fromJson(jsonString, type)
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDownload(download: Download) {
|
fun addDownload(downloadedType: DownloadedType) {
|
||||||
downloadsList.add(download)
|
downloadsList.add(downloadedType)
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeDownload(download: Download) {
|
fun removeDownload(downloadedType: DownloadedType) {
|
||||||
downloadsList.remove(download)
|
downloadsList.remove(downloadedType)
|
||||||
removeDirectory(download)
|
removeDirectory(downloadedType)
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDirectory(download: Download) {
|
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||||
val directory = if (download.type == Download.Type.MANGA){
|
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
|
"Manga"
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
"Anime"
|
||||||
} else {
|
} else {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$subDirectory/$title"
|
||||||
|
)
|
||||||
|
if (directory.exists()) {
|
||||||
|
val deleted = directory.deleteRecursively()
|
||||||
|
if (deleted) {
|
||||||
|
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
|
cleanDownloads()
|
||||||
|
}
|
||||||
|
when (type) {
|
||||||
|
DownloadedType.Type.MANGA -> {
|
||||||
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadedType.Type.ANIME -> {
|
||||||
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadedType.Type.NOVEL -> {
|
||||||
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveDownloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanDownloads() {
|
||||||
|
cleanDownload(DownloadedType.Type.MANGA)
|
||||||
|
cleanDownload(DownloadedType.Type.ANIME)
|
||||||
|
cleanDownload(DownloadedType.Type.NOVEL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanDownload(type: DownloadedType.Type) {
|
||||||
|
// remove all folders that are not in the downloads list
|
||||||
|
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||||
|
"Manga"
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
"Anime"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$subDirectory"
|
||||||
|
)
|
||||||
|
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
||||||
|
mangaDownloadedTypes
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
animeDownloadedTypes
|
||||||
|
} else {
|
||||||
|
novelDownloadedTypes
|
||||||
|
}
|
||||||
|
if (directory.exists()) {
|
||||||
|
val files = directory.listFiles()
|
||||||
|
if (files != null) {
|
||||||
|
for (file in files) {
|
||||||
|
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||||
|
val deleted = file.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//now remove all downloads that do not have a folder
|
||||||
|
val iterator = downloadsList.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val download = iterator.next()
|
||||||
|
val downloadDir = File(directory, download.title)
|
||||||
|
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
||||||
|
iterator.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||||
|
{
|
||||||
|
val jsonString = gson.toJson(downloadsList)
|
||||||
|
val file = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/downloads.json"
|
||||||
|
)
|
||||||
|
if (file.parentFile?.exists() == false) {
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
}
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.createNewFile()
|
||||||
|
}
|
||||||
|
file.writeText(jsonString)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||||
|
return downloadsList.contains(downloadedType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
||||||
|
return if (type == null) {
|
||||||
|
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||||
|
} else {
|
||||||
|
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||||
|
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the directory exists and delete it recursively
|
// Check if the directory exists and delete it recursively
|
||||||
@@ -65,13 +192,27 @@ class DownloadsManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
|
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||||
val directory = if (download.type == Download.Type.MANGA){
|
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val destination = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/${download.title}/${download.chapter}")
|
val destination = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
if (directory.exists()) {
|
if (directory.exists()) {
|
||||||
val copied = directory.copyRecursively(destination, true)
|
val copied = directory.copyRecursively(destination, true)
|
||||||
if (copied) {
|
if (copied) {
|
||||||
@@ -84,11 +225,13 @@ class DownloadsManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun purgeDownloads(type: Download.Type){
|
fun purgeDownloads(type: DownloadedType.Type) {
|
||||||
val directory = if (type == Download.Type.MANGA){
|
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||||
} else {
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||||
|
} else {
|
||||||
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||||
}
|
}
|
||||||
if (directory.exists()) {
|
if (directory.exists()) {
|
||||||
val deleted = directory.deleteRecursively()
|
val deleted = directory.deleteRecursively()
|
||||||
@@ -105,11 +248,63 @@ class DownloadsManager(private val context: Context) {
|
|||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val novelLocation = "Dantotsu/Novel"
|
||||||
|
const val mangaLocation = "Dantotsu/Manga"
|
||||||
|
const val animeLocation = "Dantotsu/Anime"
|
||||||
|
|
||||||
|
fun getDirectory(
|
||||||
|
context: Context,
|
||||||
|
type: DownloadedType.Type,
|
||||||
|
title: String,
|
||||||
|
chapter: String? = null
|
||||||
|
): File {
|
||||||
|
return if (type == DownloadedType.Type.MANGA) {
|
||||||
|
if (chapter != null) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$mangaLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$mangaLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
if (chapter != null) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$animeLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$animeLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (chapter != null) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$novelLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$novelLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class 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 {
|
enum class Type {
|
||||||
MANGA,
|
MANGA,
|
||||||
ANIME
|
ANIME,
|
||||||
|
NOVEL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?,
|
||||||
|
)
|
||||||
@@ -7,8 +7,8 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
@@ -16,41 +16,43 @@ import android.widget.Toast
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import ani.dantotsu.R
|
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.download.DownloadsManager
|
||||||
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.manga.ImageData
|
import ani.dantotsu.media.manga.ImageData
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
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.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
@@ -74,20 +76,33 @@ class MangaDownloaderService : Service() {
|
|||||||
notificationManager = NotificationManagerCompat.from(this)
|
notificationManager = NotificationManagerCompat.from(this)
|
||||||
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
|
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||||
setContentTitle("Manga Download Progress")
|
setContentTitle("Manga Download Progress")
|
||||||
setSmallIcon(R.drawable.ic_round_download_24)
|
setSmallIcon(R.drawable.ic_download_24)
|
||||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
startForeground(NOTIFICATION_ID, builder.build())
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
ContextCompat.registerReceiver(this, cancelReceiver, IntentFilter(ACTION_CANCEL_DOWNLOAD), ContextCompat.RECEIVER_NOT_EXPORTED)
|
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() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
ServiceDataSingleton.downloadQueue.clear()
|
MangaServiceDataSingleton.downloadQueue.clear()
|
||||||
downloadJobs.clear()
|
downloadJobs.clear()
|
||||||
ServiceDataSingleton.isServiceRunning = false
|
MangaServiceDataSingleton.isServiceRunning = false
|
||||||
unregisterReceiver(cancelReceiver)
|
unregisterReceiver(cancelReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +123,8 @@ class MangaDownloaderService : Service() {
|
|||||||
|
|
||||||
private fun processQueue() {
|
private fun processQueue() {
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
while (ServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
while (MangaServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||||
val task = ServiceDataSingleton.downloadQueue.poll()
|
val task = MangaServiceDataSingleton.downloadQueue.poll()
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
val job = launch { download(task) }
|
val job = launch { download(task) }
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
@@ -121,7 +136,7 @@ class MangaDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
updateNotification() // Update the notification after each task is completed
|
updateNotification() // Update the notification after each task is completed
|
||||||
}
|
}
|
||||||
if (ServiceDataSingleton.downloadQueue.isEmpty()) {
|
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
stopSelf() // Stop the service when the queue is empty
|
stopSelf() // Stop the service when the queue is empty
|
||||||
}
|
}
|
||||||
@@ -135,7 +150,7 @@ class MangaDownloaderService : Service() {
|
|||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
downloadJobs[chapter]?.cancel()
|
downloadJobs[chapter]?.cancel()
|
||||||
downloadJobs.remove(chapter)
|
downloadJobs.remove(chapter)
|
||||||
ServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
|
MangaServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
|
||||||
updateNotification() // Update the notification after cancellation
|
updateNotification() // Update the notification after cancellation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +158,7 @@ class MangaDownloaderService : Service() {
|
|||||||
|
|
||||||
private fun updateNotification() {
|
private fun updateNotification() {
|
||||||
// Update the notification to reflect the current state of the queue
|
// Update the notification to reflect the current state of the queue
|
||||||
val pendingDownloads = ServiceDataSingleton.downloadQueue.size
|
val pendingDownloads = MangaServiceDataSingleton.downloadQueue.size
|
||||||
val text = if (pendingDownloads > 0) {
|
val text = if (pendingDownloads > 0) {
|
||||||
"Pending downloads: $pendingDownloads"
|
"Pending downloads: $pendingDownloads"
|
||||||
} else {
|
} else {
|
||||||
@@ -161,74 +176,85 @@ class MangaDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun download(task: DownloadTask) {
|
suspend fun download(task: DownloadTask) {
|
||||||
withContext(Dispatchers.Main) {
|
try {
|
||||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
withContext(Dispatchers.Main) {
|
||||||
ContextCompat.checkSelfPermission(
|
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
this@MangaDownloaderService,
|
ContextCompat.checkSelfPermission(
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
this@MangaDownloaderService,
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
} else {
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
true
|
} else {
|
||||||
}
|
true
|
||||||
|
|
||||||
val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
|
||||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
|
||||||
if (notifi) {
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through each ImageData object from the task
|
|
||||||
var farthest = 0
|
|
||||||
for ((index, image) in task.imageData.withIndex()) {
|
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download the image and add to deferred list
|
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||||
val deferred = async(Dispatchers.IO) {
|
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||||
var bitmap: Bitmap? = null
|
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||||
var retryCount = 0
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
while (bitmap == null && retryCount < task.retries) {
|
// Loop through each ImageData object from the task
|
||||||
bitmap = image.fetchAndProcessImage(
|
var farthest = 0
|
||||||
image.page,
|
for ((index, image) in task.imageData.withIndex()) {
|
||||||
image.source,
|
if (deferredMap.size >= task.simultaneousDownloads) {
|
||||||
this@MangaDownloaderService
|
deferredMap.values.awaitAll()
|
||||||
|
deferredMap.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
deferredMap[index] = async(Dispatchers.IO) {
|
||||||
|
var bitmap: Bitmap? = null
|
||||||
|
var retryCount = 0
|
||||||
|
|
||||||
|
while (bitmap == null && retryCount < task.retries) {
|
||||||
|
bitmap = image.fetchAndProcessImage(
|
||||||
|
image.page,
|
||||||
|
image.source,
|
||||||
|
this@MangaDownloaderService
|
||||||
|
)
|
||||||
|
retryCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmap != null) {
|
||||||
|
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
||||||
|
}
|
||||||
|
farthest++
|
||||||
|
builder.setProgress(task.imageData.size, farthest, false)
|
||||||
|
broadcastDownloadProgress(
|
||||||
|
task.chapter,
|
||||||
|
farthest * 100 / task.imageData.size
|
||||||
)
|
)
|
||||||
retryCount++
|
if (notifi) {
|
||||||
}
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
// Cache the image if successful
|
bitmap
|
||||||
if (bitmap != null) {
|
|
||||||
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
|
||||||
}
|
}
|
||||||
farthest++
|
|
||||||
builder.setProgress(task.imageData.size, farthest, false)
|
|
||||||
broadcastDownloadProgress(task.chapter, farthest * 100 / task.imageData.size)
|
|
||||||
if (notifi) {
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
bitmap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deferredList.add(deferred)
|
// Wait for any remaining deferred to complete
|
||||||
|
deferredMap.values.awaitAll()
|
||||||
|
|
||||||
|
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
|
||||||
|
saveMediaInfo(task)
|
||||||
|
downloadsManager.addDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.chapter,
|
||||||
|
DownloadedType.Type.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
broadcastDownloadFinished(task.chapter)
|
||||||
|
snackString("${task.title} - ${task.chapter} Download finished")
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
// Wait for any remaining deferred to complete
|
logger("Exception while downloading file: ${e.message}")
|
||||||
deferredList.awaitAll()
|
snackString("Exception while downloading file: ${e.message}")
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
broadcastDownloadFailed(task.chapter)
|
||||||
.setProgress(0, 0, false)
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
|
||||||
|
|
||||||
saveMediaInfo(task)
|
|
||||||
downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA))
|
|
||||||
broadcastDownloadFinished(task.chapter)
|
|
||||||
snackString("${task.title} - ${task.chapter} Download finished")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +283,7 @@ class MangaDownloaderService : Service() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Exception while saving image: ${e.message}")
|
println("Exception while saving image: ${e.message}")
|
||||||
snackString("Exception while saving image: ${e.message}")
|
snackString("Exception while saving image: ${e.message}")
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,40 +309,54 @@ class MangaDownloaderService : Service() {
|
|||||||
|
|
||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? = withContext(Dispatchers.IO) {
|
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||||
var connection: HttpURLConnection? = null
|
withContext(Dispatchers.IO) {
|
||||||
println("Downloading url $url")
|
var connection: HttpURLConnection? = null
|
||||||
try {
|
println("Downloading url $url")
|
||||||
connection = URL(url).openConnection() as HttpURLConnection
|
try {
|
||||||
connection.connect()
|
connection = URL(url).openConnection() as HttpURLConnection
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
connection.connect()
|
||||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val file = File(directory, name)
|
||||||
|
FileOutputStream(file).use { output ->
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@withContext file.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MangaDownloaderService,
|
||||||
|
"Exception while saving ${name}: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
}
|
}
|
||||||
return@withContext file.absolutePath
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(this@MangaDownloaderService, "Exception while saving ${name}: ${e.message}", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
null
|
|
||||||
} finally {
|
|
||||||
connection?.disconnect()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun broadcastDownloadStarted(chapterNumber: String) {
|
private fun broadcastDownloadStarted(chapterNumber: String) {
|
||||||
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
|
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
|
||||||
@@ -375,10 +415,11 @@ class MangaDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ServiceDataSingleton {
|
object MangaServiceDataSingleton {
|
||||||
var imageData: List<ImageData> = listOf()
|
var imageData: List<ImageData> = listOf()
|
||||||
var sourceMedia: Media? = null
|
var sourceMedia: Media? = null
|
||||||
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var isServiceRunning: Boolean = false
|
var isServiceRunning: Boolean = false
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,30 @@
|
|||||||
package ani.dantotsu.download.manga
|
package ani.dantotsu.download.manga
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.BaseAdapter
|
import android.widget.BaseAdapter
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
|
||||||
|
|
||||||
class OfflineMangaAdapter(private val context: Context, private val items: List<OfflineMangaModel>) : BaseAdapter() {
|
class OfflineMangaAdapter(
|
||||||
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
private val context: Context,
|
||||||
|
private var items: List<OfflineMangaModel>,
|
||||||
|
private val searchListener: OfflineMangaSearchListener
|
||||||
|
) : BaseAdapter() {
|
||||||
|
private val inflater: LayoutInflater =
|
||||||
|
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
private var originalItems: List<OfflineMangaModel> = items
|
||||||
|
private var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||||
|
|
||||||
override fun getCount(): Int {
|
override fun getCount(): Int {
|
||||||
return items.size
|
return items.size
|
||||||
}
|
}
|
||||||
@@ -25,23 +37,47 @@ class OfflineMangaAdapter(private val context: Context, private val items: List<
|
|||||||
return position.toLong()
|
return position.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||||
var view = convertView
|
|
||||||
if (view == null) {
|
val view: View = convertView ?: when (style) {
|
||||||
view = inflater.inflate(R.layout.item_media_compact, parent, false)
|
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 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 titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
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
|
// 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)
|
imageView.setImageURI(item.image)
|
||||||
titleTextView.text = item.title
|
titleTextView.text = item.title
|
||||||
itemScore.text = item.score
|
itemScore.text = item.score
|
||||||
|
|
||||||
if (item.isOngoing) {
|
if (item.isOngoing) {
|
||||||
ongoing.visibility = View.VISIBLE
|
ongoing.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
@@ -49,4 +85,27 @@ class OfflineMangaAdapter(private val context: Context, private val items: List<
|
|||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onSearchQuery(query: String) {
|
||||||
|
// Implement the filtering logic here, for example:
|
||||||
|
items = if (query.isEmpty()) {
|
||||||
|
// Return the original list if the query is empty
|
||||||
|
originalItems
|
||||||
|
} else {
|
||||||
|
// Filter the list based on the query
|
||||||
|
originalItems.filter { it.title.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
notifyDataSetChanged() // Notify the adapter that the data set has changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItems(items: List<OfflineMangaModel>) {
|
||||||
|
this.items = items
|
||||||
|
this.originalItems = items
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyNewGrid() {
|
||||||
|
style = PrefManager.getVal(PrefName.OfflineView)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,62 +1,72 @@
|
|||||||
package ani.dantotsu.download.manga
|
package ani.dantotsu.download.manga
|
||||||
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.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.GridView
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.view.updatePaddingRelative
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.marginBottom
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.bottomBar
|
||||||
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.Download
|
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.logger
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.px
|
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.statusBarHeight
|
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.card.MaterialCardView
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.File
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
import kotlin.math.max
|
import uy.kohesive.injekt.Injekt
|
||||||
import kotlin.math.min
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
|
|
||||||
class OfflineMangaFragment: Fragment() {
|
|
||||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
private var downloads: List<OfflineMangaModel> = listOf()
|
private var downloads: List<OfflineMangaModel> = listOf()
|
||||||
private lateinit var gridView: GridView
|
private lateinit var gridView: GridView
|
||||||
private lateinit var adapter: OfflineMangaAdapter
|
private lateinit var adapter: OfflineMangaAdapter
|
||||||
|
private lateinit var total: TextView
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(
|
||||||
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
|
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)
|
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
|
||||||
|
textInputLayout.hint = "Manga"
|
||||||
val currentColor = textInputLayout.boxBackgroundColor
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
@@ -66,77 +76,173 @@ class OfflineMangaFragment: Fragment() {
|
|||||||
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
val color = typedValue.data
|
val color = typedValue.data
|
||||||
|
|
||||||
val animeUserAvatar= view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||||
animeUserAvatar.setSafeOnClickListener {
|
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
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||||
if (!colorOverflow) {
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
|
||||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||||
|
searchView.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
onSearchQuery(s.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var style: 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
|
||||||
}
|
}
|
||||||
|
|
||||||
gridView = view.findViewById(R.id.gridView)
|
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()
|
getDownloads()
|
||||||
adapter = OfflineMangaAdapter(requireContext(), downloads)
|
val fadeIn = AlphaAnimation(0f, 1f)
|
||||||
|
fadeIn.duration = 300 // animations pog
|
||||||
|
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||||
|
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||||
gridView.adapter = adapter
|
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
|
// Get the OfflineMangaModel that was clicked
|
||||||
val item = adapter.getItem(position) as OfflineMangaModel
|
val item = adapter.getItem(position) as OfflineMangaModel
|
||||||
val media = downloadManager.mangaDownloads.filter { it.title == item.title }.first()
|
val media =
|
||||||
startActivity(
|
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
.putExtra("media", getMedia(media))
|
media?.let {
|
||||||
.putExtra("download", true)
|
|
||||||
)
|
ContextCompat.startActivity(
|
||||||
|
requireActivity(),
|
||||||
|
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||||
|
.putExtra("media", getMedia(it))
|
||||||
|
.putExtra("download", true),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
snackString("no media found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return view
|
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
|
// Get the OfflineMangaModel that was clicked
|
||||||
|
val item = adapter.getItem(position) as OfflineMangaModel
|
||||||
|
val type: DownloadedType.Type =
|
||||||
|
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||||
|
DownloadedType.Type.MANGA
|
||||||
|
} else {
|
||||||
|
DownloadedType.Type.NOVEL
|
||||||
|
}
|
||||||
|
// Alert dialog to confirm deletion
|
||||||
|
val builder =
|
||||||
|
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||||
|
builder.setTitle("Delete ${item.title}?")
|
||||||
|
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||||
|
builder.setPositiveButton("Yes") { _, _ ->
|
||||||
|
downloadManager.removeMedia(item.title, type)
|
||||||
|
getDownloads()
|
||||||
|
adapter.setItems(downloads)
|
||||||
|
total.text =
|
||||||
|
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||||
|
}
|
||||||
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
val dialog = builder.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSearchQuery(query: String) {
|
||||||
|
adapter.onSearchQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
var height = statusBarHeight
|
initActivity(requireActivity())
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
|
||||||
if (displayCutout != null) {
|
|
||||||
if (displayCutout.boundingRects.size > 0) {
|
|
||||||
height = max(
|
|
||||||
statusBarHeight,
|
|
||||||
min(
|
|
||||||
displayCutout.boundingRects[0].width(),
|
|
||||||
displayCutout.boundingRects[0].height()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
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 {
|
scrollTop.setOnClickListener {
|
||||||
//TODO: scroll to top
|
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.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||||
|
scrollTop.translationY =
|
||||||
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
getDownloads()
|
getDownloads()
|
||||||
@@ -157,25 +263,42 @@ class OfflineMangaFragment: Fragment() {
|
|||||||
super.onStop()
|
super.onStop()
|
||||||
downloads = listOf()
|
downloads = listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDownloads() {
|
private fun getDownloads() {
|
||||||
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
|
downloads = listOf()
|
||||||
val newDownloads = mutableListOf<OfflineMangaModel>()
|
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||||
for (title in titles) {
|
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
|
for (title in mangaTitles) {
|
||||||
val download = _downloads.first()
|
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||||
|
val download = tDownloads.first()
|
||||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||||
newDownloads += offlineMangaModel
|
newMangaDownloads += offlineMangaModel
|
||||||
}
|
}
|
||||||
downloads = newDownloads
|
downloads = newMangaDownloads
|
||||||
|
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||||
|
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
|
for (title in novelTitles) {
|
||||||
|
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||||
|
val download = tDownloads.first()
|
||||||
|
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||||
|
newNovelDownloads += offlineMangaModel
|
||||||
|
}
|
||||||
|
downloads += newNovelDownloads
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMedia(download: Download): Media? {
|
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
|
val type = when (downloadedType.type) {
|
||||||
|
DownloadedType.Type.MANGA -> "Manga"
|
||||||
|
DownloadedType.Type.ANIME -> "Anime"
|
||||||
|
else -> "Novel"
|
||||||
|
}
|
||||||
val directory = File(
|
val directory = File(
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"Dantotsu/Manga/${download.title}"
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
)
|
)
|
||||||
//load media.json and convert to media class with gson
|
//load media.json and convert to media class with gson
|
||||||
try {
|
return try {
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
@@ -183,44 +306,77 @@ class OfflineMangaFragment: Fragment() {
|
|||||||
.create()
|
.create()
|
||||||
val media = File(directory, "media.json")
|
val media = File(directory, "media.json")
|
||||||
val mediaJson = media.readText()
|
val mediaJson = media.readText()
|
||||||
return gson.fromJson(mediaJson, Media::class.java)
|
gson.fromJson(mediaJson, Media::class.java)
|
||||||
}
|
} catch (e: Exception) {
|
||||||
catch (e: Exception){
|
|
||||||
logger("Error loading media.json: ${e.message}")
|
logger("Error loading media.json: ${e.message}")
|
||||||
logger(e.printStackTrace())
|
logger(e.printStackTrace())
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
return null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel{
|
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||||
|
val type = when (downloadedType.type) {
|
||||||
|
DownloadedType.Type.MANGA -> "Manga"
|
||||||
|
DownloadedType.Type.ANIME -> "Anime"
|
||||||
|
else -> "Novel"
|
||||||
|
}
|
||||||
val directory = File(
|
val directory = File(
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"Dantotsu/Manga/${download.title}"
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
)
|
)
|
||||||
//load media.json and convert to media class with gson
|
//load media.json and convert to media class with gson
|
||||||
try {
|
try {
|
||||||
val media = File(directory, "media.json")
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
val mediaJson = media.readText()
|
|
||||||
val mediaModel = getMedia(download)!!
|
|
||||||
val cover = File(directory, "cover.jpg")
|
val cover = File(directory, "cover.jpg")
|
||||||
val coverUri: Uri? = if (cover.exists()) {
|
val coverUri: Uri? = if (cover.exists()) {
|
||||||
Uri.fromFile(cover)
|
Uri.fromFile(cover)
|
||||||
} else {
|
} else null
|
||||||
null
|
val banner = File(directory, "banner.jpg")
|
||||||
}
|
val bannerUri: Uri? = if (banner.exists()) {
|
||||||
val title = mediaModel.nameMAL?:"unknown"
|
Uri.fromFile(banner)
|
||||||
val score = if (mediaModel.userScore != 0) mediaModel.userScore.toString() else
|
} else null
|
||||||
if (mediaModel.meanScore == null) "?" else mediaModel.meanScore.toString()
|
val title = mediaModel.mainName()
|
||||||
val isOngoing = false
|
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 isUserScored = mediaModel.userScore != 0
|
||||||
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
|
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||||
}
|
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||||
catch (e: Exception){
|
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("Error loading media.json: ${e.message}")
|
||||||
logger(e.printStackTrace())
|
logger(e.printStackTrace())
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
return OfflineMangaModel("unknown", "0", false, false, null)
|
return OfflineMangaModel(
|
||||||
|
"unknown",
|
||||||
|
"0",
|
||||||
|
"??",
|
||||||
|
"??",
|
||||||
|
"movie",
|
||||||
|
"hmm",
|
||||||
|
isOngoing = false,
|
||||||
|
isUserScored = false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OfflineMangaSearchListener {
|
||||||
|
fun onSearchQuery(query: String)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,5 +2,15 @@ package ani.dantotsu.download.manga
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
||||||
data class OfflineMangaModel(val title: String, val score: String, val isOngoing: Boolean, val isUserScored: Boolean, val image: Uri?) {
|
data class OfflineMangaModel(
|
||||||
}
|
val title: String,
|
||||||
|
val score: String,
|
||||||
|
val totalChapter: String,
|
||||||
|
val readChapter: String,
|
||||||
|
val type: String,
|
||||||
|
val chapters: String,
|
||||||
|
val isOngoing: Boolean,
|
||||||
|
val isUserScored: Boolean,
|
||||||
|
val image: Uri?,
|
||||||
|
val banner: Uri?
|
||||||
|
)
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
package ani.dantotsu.download.novel
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.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.gson.GsonBuilder
|
||||||
|
import com.google.gson.InstanceCreator
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Request
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.Queue
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
class NovelDownloaderService : Service() {
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
|
private lateinit var builder: NotificationCompat.Builder
|
||||||
|
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
|
private val downloadJobs = mutableMapOf<String, Job>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private var isCurrentlyProcessing = false
|
||||||
|
|
||||||
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
// This is only required for bound services.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
notificationManager = NotificationManagerCompat.from(this)
|
||||||
|
builder =
|
||||||
|
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||||
|
setContentTitle("Novel Download Progress")
|
||||||
|
setSmallIcon(R.drawable.ic_download_24)
|
||||||
|
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
builder.build(),
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
this,
|
||||||
|
cancelReceiver,
|
||||||
|
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
NovelServiceDataSingleton.downloadQueue.clear()
|
||||||
|
downloadJobs.clear()
|
||||||
|
NovelServiceDataSingleton.isServiceRunning = false
|
||||||
|
unregisterReceiver(cancelReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
snackString("Download started")
|
||||||
|
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
serviceScope.launch {
|
||||||
|
mutex.withLock {
|
||||||
|
if (!isCurrentlyProcessing) {
|
||||||
|
isCurrentlyProcessing = true
|
||||||
|
processQueue()
|
||||||
|
isCurrentlyProcessing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processQueue() {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (NovelServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||||
|
val task = NovelServiceDataSingleton.downloadQueue.poll()
|
||||||
|
if (task != null) {
|
||||||
|
val job = launch { download(task) }
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[task.chapter] = job
|
||||||
|
}
|
||||||
|
job.join() // Wait for the job to complete before continuing to the next task
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs.remove(task.chapter)
|
||||||
|
}
|
||||||
|
updateNotification() // Update the notification after each task is completed
|
||||||
|
}
|
||||||
|
if (NovelServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
stopSelf() // Stop the service when the queue is empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelDownload(chapter: String) {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[chapter]?.cancel()
|
||||||
|
downloadJobs.remove(chapter)
|
||||||
|
NovelServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
|
||||||
|
updateNotification() // Update the notification after cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification() {
|
||||||
|
// Update the notification to reflect the current state of the queue
|
||||||
|
val pendingDownloads = NovelServiceDataSingleton.downloadQueue.size
|
||||||
|
val text = if (pendingDownloads > 0) {
|
||||||
|
"Pending downloads: $pendingDownloads"
|
||||||
|
} else {
|
||||||
|
"All downloads completed"
|
||||||
|
}
|
||||||
|
builder.setContentText(text)
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun isEpubFile(urlString: String): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(urlString)
|
||||||
|
.head()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
networkHelper.client.newCall(request).execute().use { response ->
|
||||||
|
val contentType = response.header("Content-Type")
|
||||||
|
val contentDisposition = response.header("Content-Disposition")
|
||||||
|
|
||||||
|
logger("Content-Type: $contentType")
|
||||||
|
logger("Content-Disposition: $contentDisposition")
|
||||||
|
|
||||||
|
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
|
||||||
|
contentType == "application/epub+zip" ||
|
||||||
|
(contentDisposition?.contains(".epub") == true)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Error checking file type: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAlreadyDownloaded(urlString: String): Boolean {
|
||||||
|
return urlString.contains("file://")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun download(task: DownloadTask) {
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
this@NovelDownloaderService,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastDownloadStarted(task.originalLink)
|
||||||
|
|
||||||
|
if (notifi) {
|
||||||
|
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEpubFile(task.downloadLink)) {
|
||||||
|
if (isAlreadyDownloaded(task.originalLink)) {
|
||||||
|
logger("Already downloaded")
|
||||||
|
broadcastDownloadFinished(task.originalLink)
|
||||||
|
snackString("Already downloaded")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
logger("Download link is not an .epub file")
|
||||||
|
broadcastDownloadFailed(task.originalLink)
|
||||||
|
snackString("Download link is not an .epub file")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the download
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(task.downloadLink)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
networkHelper.downloadClient.newCall(request).execute().use { response ->
|
||||||
|
// Ensure the response is successful and has a body
|
||||||
|
if (!response.isSuccessful || response.body == null) {
|
||||||
|
throw IOException("Failed to download file: ${response.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(
|
||||||
|
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create directories if they don't exist
|
||||||
|
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
|
||||||
|
|
||||||
|
// Overwrite existing file
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
|
||||||
|
//download cover
|
||||||
|
task.coverUrl?.let {
|
||||||
|
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
||||||
|
}
|
||||||
|
|
||||||
|
val sink = file.sink().buffer()
|
||||||
|
val responseBody = response.body
|
||||||
|
val totalBytes = responseBody.contentLength()
|
||||||
|
var downloadedBytes = 0L
|
||||||
|
|
||||||
|
val notificationUpdateInterval = 1024 * 1024 // 1 MB
|
||||||
|
val broadcastUpdateInterval = 1024 * 256 // 256 KB
|
||||||
|
var lastNotificationUpdate = 0L
|
||||||
|
var lastBroadcastUpdate = 0L
|
||||||
|
|
||||||
|
responseBody.source().use { source ->
|
||||||
|
while (true) {
|
||||||
|
val read = source.read(sink.buffer, 8192)
|
||||||
|
if (read == -1L) break
|
||||||
|
downloadedBytes += read
|
||||||
|
sink.emit()
|
||||||
|
|
||||||
|
// Update progress at intervals
|
||||||
|
if (downloadedBytes - lastNotificationUpdate >= notificationUpdateInterval) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val progress =
|
||||||
|
(downloadedBytes * 100 / totalBytes).toInt()
|
||||||
|
builder.setProgress(100, progress, false)
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
builder.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastNotificationUpdate = downloadedBytes
|
||||||
|
}
|
||||||
|
if (downloadedBytes - lastBroadcastUpdate >= broadcastUpdateInterval) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val progress =
|
||||||
|
(downloadedBytes * 100 / totalBytes).toInt()
|
||||||
|
logger("Download progress: $progress")
|
||||||
|
broadcastDownloadProgress(task.originalLink, progress)
|
||||||
|
}
|
||||||
|
lastBroadcastUpdate = downloadedBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sink.close()
|
||||||
|
//if the file is smaller than 95% of totalBytes, it means the download was interrupted
|
||||||
|
if (file.length() < totalBytes * 0.95) {
|
||||||
|
throw IOException("Failed to download file: ${response.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Exception while downloading .epub inside request: ${e.message}")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update notification for download completion
|
||||||
|
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMediaInfo(task)
|
||||||
|
downloadsManager.addDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.chapter,
|
||||||
|
DownloadedType.Type.NOVEL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
broadcastDownloadFinished(task.originalLink)
|
||||||
|
snackString("${task.title} - ${task.chapter} Download finished")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Exception while downloading .epub: ${e.message}")
|
||||||
|
snackString("Exception while downloading .epub: ${e.message}")
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
|
broadcastDownloadFailed(task.originalLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val directory = File(
|
||||||
|
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${task.title}"
|
||||||
|
)
|
||||||
|
if (!directory.exists()) directory.mkdirs()
|
||||||
|
|
||||||
|
val file = File(directory, "media.json")
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
val mediaJson = gson.toJson(task.sourceMedia)
|
||||||
|
val media = gson.fromJson(mediaJson, Media::class.java)
|
||||||
|
if (media != null) {
|
||||||
|
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
|
||||||
|
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
|
||||||
|
|
||||||
|
val jsonString = gson.toJson(media)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
file.writeText(jsonString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||||
|
withContext(
|
||||||
|
Dispatchers.IO
|
||||||
|
) {
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
println("Downloading url $url")
|
||||||
|
try {
|
||||||
|
connection = URL(url).openConnection() as HttpURLConnection
|
||||||
|
connection.connect()
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(directory, name)
|
||||||
|
FileOutputStream(file).use { output ->
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@withContext file.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@NovelDownloaderService,
|
||||||
|
"Exception while saving ${name}: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadStarted(link: String) {
|
||||||
|
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_STARTED).apply {
|
||||||
|
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFinished(link: String) {
|
||||||
|
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FINISHED).apply {
|
||||||
|
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFailed(link: String) {
|
||||||
|
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FAILED).apply {
|
||||||
|
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadProgress(link: String, progress: Int) {
|
||||||
|
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_PROGRESS).apply {
|
||||||
|
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
|
||||||
|
putExtra("progress", progress)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
|
||||||
|
val chapter = intent.getStringExtra(EXTRA_CHAPTER)
|
||||||
|
chapter?.let {
|
||||||
|
cancelDownload(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class DownloadTask(
|
||||||
|
val title: String,
|
||||||
|
val chapter: String,
|
||||||
|
val downloadLink: String,
|
||||||
|
val originalLink: String,
|
||||||
|
val sourceMedia: Media? = null,
|
||||||
|
val coverUrl: String? = null,
|
||||||
|
val retries: Int = 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_ID = 1103
|
||||||
|
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
|
||||||
|
const val EXTRA_CHAPTER = "extra_chapter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object NovelServiceDataSingleton {
|
||||||
|
var sourceMedia: Media? = null
|
||||||
|
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var isServiceRunning: Boolean = false
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) {
|
class ExoplayerDownloadService :
|
||||||
|
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val JOB_ID = 1
|
private const val JOB_ID = 1
|
||||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||||
@@ -21,7 +22,10 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
|
|||||||
|
|
||||||
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
||||||
|
|
||||||
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
|
override fun getForegroundNotification(
|
||||||
|
downloads: MutableList<Download>,
|
||||||
|
notMetRequirements: Int
|
||||||
|
): Notification =
|
||||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||||
this,
|
this,
|
||||||
R.drawable.mono,
|
R.drawable.mono,
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
package ani.dantotsu.download.video
|
package ani.dantotsu.download.video
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
import androidx.media3.common.TrackSelectionParameters
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.database.StandaloneDatabaseProvider
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
import androidx.media3.datasource.DataSource
|
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.cache.SimpleCache
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
|
import androidx.media3.exoplayer.offline.Download
|
||||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||||
import androidx.media3.exoplayer.offline.DownloadManager
|
import androidx.media3.exoplayer.offline.DownloadManager
|
||||||
import androidx.media3.exoplayer.offline.DownloadService
|
import androidx.media3.exoplayer.offline.DownloadService
|
||||||
import androidx.media3.exoplayer.scheduler.Requirements
|
import androidx.media3.exoplayer.scheduler.Requirements
|
||||||
import androidx.media3.ui.TrackSelectionDialogBuilder
|
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.defaultHeaders
|
import ani.dantotsu.defaultHeaders
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
|
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.okHttpClient
|
import ani.dantotsu.okHttpClient
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
import ani.dantotsu.parsers.SubtitleType
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
import ani.dantotsu.parsers.Video
|
import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.parsers.VideoType
|
import ani.dantotsu.parsers.VideoType
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -37,10 +52,13 @@ import java.util.concurrent.*
|
|||||||
|
|
||||||
object Helper {
|
object Helper {
|
||||||
|
|
||||||
|
private var simpleCache: SimpleCache? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
fun downloadVideo(context : Context, video: Video, subtitle: Subtitle?){
|
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||||
val dataSourceFactory = DataSource.Factory {
|
val dataSourceFactory = DataSource.Factory {
|
||||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
val dataSource: HttpDataSource =
|
||||||
|
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
defaultHeaders.forEach {
|
defaultHeaders.forEach {
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
dataSource.setRequestProperty(it.key, it.value)
|
||||||
}
|
}
|
||||||
@@ -52,7 +70,7 @@ object Helper {
|
|||||||
val mimeType = when (video.format) {
|
val mimeType = when (video.format) {
|
||||||
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||||
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
||||||
else -> MimeTypes.APPLICATION_MP4
|
else -> MimeTypes.APPLICATION_MP4
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
||||||
@@ -79,28 +97,15 @@ object Helper {
|
|||||||
DefaultRenderersFactory(context),
|
DefaultRenderersFactory(context),
|
||||||
dataSourceFactory
|
dataSourceFactory
|
||||||
)
|
)
|
||||||
downloadHelper.prepare(object : DownloadHelper.Callback{
|
downloadHelper.prepare(object : DownloadHelper.Callback {
|
||||||
override fun onPrepared(helper: DownloadHelper) {
|
override fun onPrepared(helper: DownloadHelper) {
|
||||||
TrackSelectionDialogBuilder(context,"Select thingy",helper.getTracks(0).groups
|
helper.getDownloadRequest(null).let {
|
||||||
) { _, overrides ->
|
|
||||||
val params = TrackSelectionParameters.Builder(context)
|
|
||||||
overrides.forEach{
|
|
||||||
params.addOverride(it.value)
|
|
||||||
}
|
|
||||||
helper.addTrackSelection(0, params.build())
|
|
||||||
MyDownloadService
|
|
||||||
DownloadService.sendAddDownload(
|
DownloadService.sendAddDownload(
|
||||||
context,
|
context,
|
||||||
MyDownloadService::class.java,
|
ExoplayerDownloadService::class.java,
|
||||||
helper.getDownloadRequest(null),
|
it,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}.apply {
|
|
||||||
setTheme(R.style.DialogTheme)
|
|
||||||
setTrackNameProvider {
|
|
||||||
if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
|
|
||||||
}
|
|
||||||
build().show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,34 +117,61 @@ object Helper {
|
|||||||
|
|
||||||
|
|
||||||
private var download: DownloadManager? = null
|
private var download: DownloadManager? = null
|
||||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads"
|
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun downloadManager(context: Context): DownloadManager {
|
fun downloadManager(context: Context): DownloadManager {
|
||||||
return download ?: let {
|
return download ?: let {
|
||||||
val database = StandaloneDatabaseProvider(context)
|
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||||
val dataSourceFactory = DataSource.Factory {
|
val dataSourceFactory = DataSource.Factory {
|
||||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
val okHttpClient = networkHelper.client
|
val okHttpClient = networkHelper.client
|
||||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
val dataSource: HttpDataSource =
|
||||||
|
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
defaultHeaders.forEach {
|
defaultHeaders.forEach {
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
dataSource.setRequestProperty(it.key, it.value)
|
||||||
}
|
}
|
||||||
dataSource
|
dataSource
|
||||||
}
|
}
|
||||||
DownloadManager(
|
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||||
|
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||||
|
val downloadManager = DownloadManager(
|
||||||
context,
|
context,
|
||||||
database,
|
database,
|
||||||
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database),
|
getSimpleCache(context),
|
||||||
dataSourceFactory,
|
dataSourceFactory,
|
||||||
Executor(Runnable::run)
|
executorService
|
||||||
).apply {
|
).apply {
|
||||||
requirements = Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
requirements =
|
||||||
|
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||||
maxParallelDownloads = 3
|
maxParallelDownloads = 3
|
||||||
}
|
}
|
||||||
|
downloadManager.addListener( //for testing
|
||||||
|
object : DownloadManager.Listener {
|
||||||
|
override fun onDownloadChanged(
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
download: Download,
|
||||||
|
finalException: Exception?
|
||||||
|
) {
|
||||||
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
|
Log.e("Downloader", "Download Completed")
|
||||||
|
} else if (download.state == Download.STATE_FAILED) {
|
||||||
|
Log.e("Downloader", "Download Failed")
|
||||||
|
} else if (download.state == Download.STATE_STOPPED) {
|
||||||
|
Log.e("Downloader", "Download Stopped")
|
||||||
|
} else if (download.state == Download.STATE_QUEUED) {
|
||||||
|
Log.e("Downloader", "Download Queued")
|
||||||
|
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||||
|
Log.e("Downloader", "Download Downloading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
downloadManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,4 +187,102 @@ object Helper {
|
|||||||
}
|
}
|
||||||
return downloadDirectory!!
|
return downloadDirectory!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun startAnimeDownloadService(
|
||||||
|
context: Context,
|
||||||
|
title: String,
|
||||||
|
episode: String,
|
||||||
|
video: Video,
|
||||||
|
subtitle: Subtitle? = null,
|
||||||
|
sourceMedia: Media? = null,
|
||||||
|
episodeImage: String? = null
|
||||||
|
) {
|
||||||
|
if (!isNotificationPermissionGranted(context)) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
context as Activity,
|
||||||
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
|
||||||
|
title,
|
||||||
|
episode,
|
||||||
|
video,
|
||||||
|
subtitle,
|
||||||
|
sourceMedia,
|
||||||
|
episodeImage
|
||||||
|
)
|
||||||
|
|
||||||
|
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||||
|
val downloadCheck = downloadsManger
|
||||||
|
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
||||||
|
|
||||||
|
if (downloadCheck) {
|
||||||
|
AlertDialog.Builder(context, R.style.MyPopup)
|
||||||
|
.setTitle("Download Exists")
|
||||||
|
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||||
|
.setPositiveButton("Yes") { _, _ ->
|
||||||
|
DownloadService.sendRemoveDownload(
|
||||||
|
context,
|
||||||
|
ExoplayerDownloadService::class.java,
|
||||||
|
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.SearchResults
|
||||||
import ani.dantotsu.connections.anilist.getUserId
|
import ani.dantotsu.connections.anilist.getUserId
|
||||||
import ani.dantotsu.databinding.FragmentAnimeBinding
|
import ani.dantotsu.databinding.FragmentAnimeBinding
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.ProgressAdapter
|
import ani.dantotsu.media.ProgressAdapter
|
||||||
import ani.dantotsu.media.SearchActivity
|
import ani.dantotsu.media.SearchActivity
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.px
|
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.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -47,8 +47,7 @@ import kotlin.math.min
|
|||||||
class AnimeFragment : Fragment() {
|
class AnimeFragment : Fragment() {
|
||||||
private var _binding: FragmentAnimeBinding? = null
|
private var _binding: FragmentAnimeBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
private lateinit var animePageAdapter: AnimePageAdapter
|
||||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
|
|
||||||
val model: AnilistAnimeViewModel by activityViewModels()
|
val model: AnilistAnimeViewModel by activityViewModels()
|
||||||
|
|
||||||
@@ -94,7 +93,7 @@ class AnimeFragment : Fragment() {
|
|||||||
|
|
||||||
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||||
|
|
||||||
val animePageAdapter = AnimePageAdapter()
|
animePageAdapter = AnimePageAdapter()
|
||||||
|
|
||||||
var loading = true
|
var loading = true
|
||||||
if (model.notSet) {
|
if (model.notSet) {
|
||||||
@@ -214,7 +213,7 @@ class AnimeFragment : Fragment() {
|
|||||||
if (it != null) {
|
if (it != null) {
|
||||||
animePageAdapter.updateTrending(
|
animePageAdapter.updateTrending(
|
||||||
MediaAdaptor(
|
MediaAdaptor(
|
||||||
if (uiSettings.smallView) 3 else 2,
|
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
|
||||||
it,
|
it,
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
viewPager = animePageAdapter.trendingViewPager
|
viewPager = animePageAdapter.trendingViewPager
|
||||||
@@ -224,7 +223,8 @@ class AnimeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.animePageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
binding.animePageScrollTop.translationY =
|
||||||
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +264,11 @@ class AnimeFragment : Fragment() {
|
|||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.loadTrending(1)
|
model.loadTrending(1)
|
||||||
model.loadUpdated()
|
model.loadUpdated()
|
||||||
model.loadPopular("ANIME", sort = Anilist.sortBy[1])
|
model.loadPopular(
|
||||||
|
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||||
|
PrefName.PopularAnimeList
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
live.postValue(false)
|
live.postValue(false)
|
||||||
_binding?.animeRefresh?.isRefreshing = false
|
_binding?.animeRefresh?.isRefreshing = false
|
||||||
@@ -275,6 +279,11 @@ class AnimeFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||||
|
if (animePageAdapter.trendingViewPager != null) {
|
||||||
|
binding.root.requestApplyInsets()
|
||||||
|
binding.root.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
@@ -18,15 +16,14 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.media.GenreActivity
|
|
||||||
import ani.dantotsu.MediaPageTransformer
|
import ani.dantotsu.MediaPageTransformer
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.CalendarActivity
|
import ani.dantotsu.media.CalendarActivity
|
||||||
|
import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.SearchActivity
|
import ani.dantotsu.media.SearchActivity
|
||||||
import ani.dantotsu.px
|
import ani.dantotsu.px
|
||||||
@@ -34,7 +31,8 @@ import ani.dantotsu.setSafeOnClickListener
|
|||||||
import ani.dantotsu.setSlideIn
|
import ani.dantotsu.setSlideIn
|
||||||
import ani.dantotsu.setSlideUp
|
import ani.dantotsu.setSlideUp
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
@@ -45,10 +43,10 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
private var trendHandler: Handler? = null
|
private var trendHandler: Handler? = null
|
||||||
private lateinit var trendRun: Runnable
|
private lateinit var trendRun: Runnable
|
||||||
var trendingViewPager: ViewPager2? = null
|
var trendingViewPager: ViewPager2? = null
|
||||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
|
||||||
val binding = ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return AnimePageViewHolder(binding)
|
return AnimePageViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,22 +58,19 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
val currentColor = textInputLayout.boxBackgroundColor
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
val materialCardView =
|
||||||
|
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
val color = typedValue.data
|
val color = typedValue.data
|
||||||
|
|
||||||
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||||
if (!colorOverflow) {
|
|
||||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
|
||||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = (-108f).px
|
bottomMargin = (-108f).px
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +90,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.animeUserAvatar.setSafeOnClickListener {
|
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(
|
listOf(
|
||||||
@@ -125,17 +122,23 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE
|
binding.animeIncludeList.visibility =
|
||||||
|
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
|
||||||
|
|
||||||
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||||
onIncludeListClick.invoke(isChecked)
|
onIncludeListClick.invoke(isChecked)
|
||||||
|
|
||||||
|
PrefManager.setVal(PrefName.PopularAnimeList, isChecked)
|
||||||
}
|
}
|
||||||
if (ready.value == false)
|
if (ready.value == false)
|
||||||
ready.postValue(true)
|
ready.postValue(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var onSeasonClick : ((Int)->Unit)
|
lateinit var onSeasonClick: ((Int) -> Unit)
|
||||||
lateinit var onSeasonLongClick : ((Int)->Boolean)
|
lateinit var onSeasonLongClick: ((Int) -> Boolean)
|
||||||
lateinit var onIncludeListClick : ((Boolean)->Unit)
|
lateinit var onIncludeListClick: ((Boolean) -> Unit)
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
@@ -152,7 +155,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
|
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
binding.animeTrendingViewPager.currentItem = binding.animeTrendingViewPager.currentItem + 1
|
binding.animeTrendingViewPager.currentItem =
|
||||||
|
binding.animeTrendingViewPager.currentItem + 1
|
||||||
}
|
}
|
||||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
@@ -164,24 +168,32 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.animeTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.animeTrendingViewPager.layoutAnimation =
|
||||||
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.animeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.animeTitleContainer.startAnimation(setSlideUp())
|
||||||
binding.animeSeasonsCont.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.animeListContainer.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
|
binding.animeSeasonsCont.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateRecent(adaptor: MediaAdaptor) {
|
fun updateRecent(adaptor: MediaAdaptor) {
|
||||||
binding.animeUpdatedProgressBar.visibility = View.GONE
|
binding.animeUpdatedProgressBar.visibility = View.GONE
|
||||||
binding.animeUpdatedRecyclerView.adapter = adaptor
|
binding.animeUpdatedRecyclerView.adapter = adaptor
|
||||||
binding.animeUpdatedRecyclerView.layoutManager =
|
binding.animeUpdatedRecyclerView.layoutManager =
|
||||||
LinearLayoutManager(binding.animeUpdatedRecyclerView.context, LinearLayoutManager.HORIZONTAL, false)
|
LinearLayoutManager(
|
||||||
|
binding.animeUpdatedRecyclerView.context,
|
||||||
|
LinearLayoutManager.HORIZONTAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||||
|
|
||||||
binding.animeRecently.visibility = View.VISIBLE
|
binding.animeRecently.visibility = View.VISIBLE
|
||||||
binding.animeRecently.startAnimation(setSlideUp(uiSettings))
|
binding.animeRecently.startAnimation(setSlideUp())
|
||||||
binding.animeUpdatedRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.animeUpdatedRecyclerView.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.animePopular.visibility = View.VISIBLE
|
binding.animePopular.visibility = View.VISIBLE
|
||||||
binding.animePopular.startAnimation(setSlideUp(uiSettings))
|
binding.animePopular.startAnimation(setSlideUp())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAvatar() {
|
fun updateAvatar() {
|
||||||
@@ -191,5 +203,6 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,17 +27,17 @@ import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
|||||||
import ani.dantotsu.connections.anilist.getUserId
|
import ani.dantotsu.connections.anilist.getUserId
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.FragmentHomeBinding
|
import ani.dantotsu.databinding.FragmentHomeBinding
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
|
||||||
import ani.dantotsu.media.user.ListActivity
|
import ani.dantotsu.media.user.ListActivity
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.setSlideIn
|
import ani.dantotsu.setSlideIn
|
||||||
import ani.dantotsu.setSlideUp
|
import ani.dantotsu.setSlideUp
|
||||||
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -52,7 +52,11 @@ class HomeFragment : Fragment() {
|
|||||||
private var _binding: FragmentHomeBinding? = null
|
private var _binding: FragmentHomeBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -66,14 +70,13 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val scope = lifecycleScope
|
val scope = lifecycleScope
|
||||||
var uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
fun load() {
|
fun load() {
|
||||||
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
|
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
|
||||||
binding.homeUserName.text = Anilist.username
|
binding.homeUserName.text = Anilist.username
|
||||||
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
||||||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
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.homeUserBg.loadImage(Anilist.bg)
|
||||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||||
|
|
||||||
@@ -94,20 +97,26 @@ class HomeFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||||
binding.homeUserDataContainer.visibility = View.VISIBLE
|
binding.homeUserDataContainer.visibility = View.VISIBLE
|
||||||
binding.homeUserDataContainer.layoutAnimation = LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
|
binding.homeUserDataContainer.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideUp(), 0.25f)
|
||||||
binding.homeAnimeList.visibility = View.VISIBLE
|
binding.homeAnimeList.visibility = View.VISIBLE
|
||||||
binding.homeMangaList.visibility = View.VISIBLE
|
binding.homeMangaList.visibility = View.VISIBLE
|
||||||
binding.homeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.homeListContainer.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
snackString(currContext()?.getString(R.string.please_reload))
|
snackString(currContext()?.getString(R.string.please_reload))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
||||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show(parentFragmentManager, "dialog")
|
val dialogFragment =
|
||||||
|
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
|
||||||
|
dialogFragment.show(
|
||||||
|
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
|
||||||
|
"dialog"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -117,18 +126,18 @@ class HomeFragment : Fragment() {
|
|||||||
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
var reached = false
|
var reached = false
|
||||||
val duration = (uiSettings.animationSpeed * 200).toLong()
|
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
reached = true
|
||||||
reached = true
|
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration).start()
|
.start()
|
||||||
} else {
|
} else {
|
||||||
if (reached) {
|
if (reached) {
|
||||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration).start()
|
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||||
}
|
.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +147,13 @@ class HomeFragment : Fragment() {
|
|||||||
if (displayCutout != null) {
|
if (displayCutout != null) {
|
||||||
if (displayCutout.boundingRects.size > 0) {
|
if (displayCutout.boundingRects.size > 0) {
|
||||||
height =
|
height =
|
||||||
max(statusBarHeight, min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height()))
|
max(
|
||||||
|
statusBarHeight,
|
||||||
|
min(
|
||||||
|
displayCutout.boundingRects[0].width(),
|
||||||
|
displayCutout.boundingRects[0].height()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,13 +204,14 @@ class HomeFragment : Fragment() {
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
recyclerView.visibility = View.VISIBLE
|
recyclerView.visibility = View.VISIBLE
|
||||||
recyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
recyclerView.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
empty.visibility = View.VISIBLE
|
empty.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
title.visibility = View.VISIBLE
|
title.visibility = View.VISIBLE
|
||||||
title.startAnimation(setSlideUp(uiSettings))
|
title.startAnimation(setSlideUp())
|
||||||
progress.visibility = View.GONE
|
progress.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,25 +294,25 @@ class HomeFragment : Fragment() {
|
|||||||
binding.homeRecommended
|
binding.homeRecommended
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||||
|
|
||||||
model.empty.observe(viewLifecycleOwner) {
|
model.empty.observe(viewLifecycleOwner) {
|
||||||
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
|
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
|
||||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||||
binding.homeDantotsuContainer.startAnimation(setSlideUp(uiSettings))
|
binding.homeDantotsuContainer.startAnimation(setSlideUp())
|
||||||
binding.homeDantotsuIcon.setSafeOnClickListener {
|
binding.homeDantotsuIcon.setSafeOnClickListener {
|
||||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val array = arrayOf(
|
val array = arrayOf(
|
||||||
Runnable { runBlocking { model.setAnimeContinue() } },
|
"AnimeContinue",
|
||||||
Runnable { runBlocking { model.setAnimeFav() } },
|
"AnimeFav",
|
||||||
Runnable { runBlocking { model.setAnimePlanned() } },
|
"AnimePlanned",
|
||||||
Runnable { runBlocking { model.setMangaContinue() } },
|
"MangaContinue",
|
||||||
Runnable { runBlocking { model.setMangaFav() } },
|
"MangaFav",
|
||||||
Runnable { runBlocking { model.setMangaPlanned() } },
|
"MangaPlanned",
|
||||||
Runnable { runBlocking { model.setRecommendation() } }
|
"Recommendation"
|
||||||
)
|
)
|
||||||
|
|
||||||
val containers = arrayOf(
|
val containers = arrayOf(
|
||||||
@@ -313,7 +329,6 @@ class HomeFragment : Fragment() {
|
|||||||
live.observe(viewLifecycleOwner) {
|
live.observe(viewLifecycleOwner) {
|
||||||
if (it) {
|
if (it) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
//Get userData First
|
//Get userData First
|
||||||
getUserId(requireContext()) {
|
getUserId(requireContext()) {
|
||||||
@@ -322,9 +337,13 @@ class HomeFragment : Fragment() {
|
|||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.setListImages()
|
model.setListImages()
|
||||||
var empty = true
|
var empty = true
|
||||||
|
val homeLayoutShow: List<Boolean> =
|
||||||
|
PrefManager.getVal(PrefName.HomeLayoutShow)
|
||||||
|
runBlocking {
|
||||||
|
model.initHomePage()
|
||||||
|
}
|
||||||
(array.indices).forEach { i ->
|
(array.indices).forEach { i ->
|
||||||
if (uiSettings.homeLayoutShow[i]) {
|
if (homeLayoutShow.elementAt(i)) {
|
||||||
array[i].run()
|
|
||||||
empty = false
|
empty = false
|
||||||
} else withContext(Dispatchers.Main) {
|
} else withContext(Dispatchers.Main) {
|
||||||
containers[i].visibility = View.GONE
|
containers[i].visibility = View.GONE
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.databinding.FragmentLoginBinding
|
import ani.dantotsu.databinding.FragmentLoginBinding
|
||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||||
|
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||||
|
import ani.dantotsu.toast
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
|
||||||
class LoginFragment : Fragment() {
|
class LoginFragment : Fragment() {
|
||||||
|
|
||||||
private var _binding: FragmentLoginBinding? = null
|
private var _binding: FragmentLoginBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = FragmentLoginBinding.inflate(layoutInflater, container, false)
|
_binding = FragmentLoginBinding.inflate(layoutInflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -24,5 +37,100 @@ class LoginFragment : Fragment() {
|
|||||||
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
|
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
|
||||||
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
|
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
|
||||||
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
|
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
|
||||||
|
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
|
||||||
|
|
||||||
|
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.SearchResults
|
||||||
import ani.dantotsu.connections.anilist.getUserId
|
import ani.dantotsu.connections.anilist.getUserId
|
||||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
import ani.dantotsu.databinding.FragmentMangaBinding
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.ProgressAdapter
|
import ani.dantotsu.media.ProgressAdapter
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.px
|
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.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -43,12 +43,15 @@ import kotlin.math.min
|
|||||||
class MangaFragment : Fragment() {
|
class MangaFragment : Fragment() {
|
||||||
private var _binding: FragmentMangaBinding? = null
|
private var _binding: FragmentMangaBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
private lateinit var mangaPageAdapter: MangaPageAdapter
|
||||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
|
|
||||||
val model: AnilistMangaViewModel by activityViewModels()
|
val model: AnilistMangaViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = FragmentMangaBinding.inflate(inflater, container, false)
|
_binding = FragmentMangaBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -85,7 +88,7 @@ class MangaFragment : Fragment() {
|
|||||||
|
|
||||||
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||||
|
|
||||||
val mangaPageAdapter = MangaPageAdapter()
|
mangaPageAdapter = MangaPageAdapter()
|
||||||
var loading = true
|
var loading = true
|
||||||
if (model.notSet) {
|
if (model.notSet) {
|
||||||
model.notSet = false
|
model.notSet = false
|
||||||
@@ -100,7 +103,8 @@ class MangaFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
|
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
|
||||||
val progressAdaptor = ProgressAdapter(searched = model.searched)
|
val progressAdaptor = ProgressAdapter(searched = model.searched)
|
||||||
binding.mangaPageRecyclerView.adapter = ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
binding.mangaPageRecyclerView.adapter =
|
||||||
|
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
||||||
val layout = LinearLayoutManager(requireContext())
|
val layout = LinearLayoutManager(requireContext())
|
||||||
binding.mangaPageRecyclerView.layoutManager = layout
|
binding.mangaPageRecyclerView.layoutManager = layout
|
||||||
|
|
||||||
@@ -167,7 +171,7 @@ class MangaFragment : Fragment() {
|
|||||||
if (it != null) {
|
if (it != null) {
|
||||||
mangaPageAdapter.updateTrending(
|
mangaPageAdapter.updateTrending(
|
||||||
MediaAdaptor(
|
MediaAdaptor(
|
||||||
if (uiSettings.smallView) 3 else 2,
|
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
|
||||||
it,
|
it,
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
viewPager = mangaPageAdapter.trendingViewPager
|
viewPager = mangaPageAdapter.trendingViewPager
|
||||||
@@ -177,7 +181,8 @@ class MangaFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.mangaPageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
binding.mangaPageScrollTop.translationY =
|
||||||
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +238,11 @@ class MangaFragment : Fragment() {
|
|||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.loadTrending()
|
model.loadTrending()
|
||||||
model.loadTrendingNovel()
|
model.loadTrendingNovel()
|
||||||
model.loadPopular("MANGA", sort = Anilist.sortBy[1])
|
model.loadPopular(
|
||||||
|
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||||
|
PrefName.PopularMangaList
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
live.postValue(false)
|
live.postValue(false)
|
||||||
_binding?.mangaRefresh?.isRefreshing = false
|
_binding?.mangaRefresh?.isRefreshing = false
|
||||||
@@ -244,6 +253,11 @@ class MangaFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||||
|
//make sure mangaPageAdapter is initialized
|
||||||
|
if (mangaPageAdapter.trendingViewPager != null) {
|
||||||
|
binding.root.requestApplyInsets()
|
||||||
|
binding.root.requestLayout()
|
||||||
|
}
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -17,14 +16,13 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.media.GenreActivity
|
|
||||||
import ani.dantotsu.MediaPageTransformer
|
import ani.dantotsu.MediaPageTransformer
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.SearchActivity
|
import ani.dantotsu.media.SearchActivity
|
||||||
import ani.dantotsu.px
|
import ani.dantotsu.px
|
||||||
@@ -32,7 +30,8 @@ import ani.dantotsu.setSafeOnClickListener
|
|||||||
import ani.dantotsu.setSlideIn
|
import ani.dantotsu.setSlideIn
|
||||||
import ani.dantotsu.setSlideUp
|
import ani.dantotsu.setSlideUp
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
@@ -43,10 +42,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
private var trendHandler: Handler? = null
|
private var trendHandler: Handler? = null
|
||||||
private lateinit var trendRun: Runnable
|
private lateinit var trendRun: Runnable
|
||||||
var trendingViewPager: ViewPager2? = null
|
var trendingViewPager: ViewPager2? = null
|
||||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
|
||||||
val binding = ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return MangaPageViewHolder(binding)
|
return MangaPageViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,22 +57,19 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
val currentColor = textInputLayout.boxBackgroundColor
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
val materialCardView =
|
||||||
|
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
val color = typedValue.data
|
val color = typedValue.data
|
||||||
|
|
||||||
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||||
if (!colorOverflow) {
|
|
||||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
|
||||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = (-108f).px
|
bottomMargin = (-108f).px
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +85,9 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
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 {
|
binding.mangaSearchBar.setEndIconOnClickListener {
|
||||||
@@ -117,16 +115,21 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE
|
binding.mangaIncludeList.visibility =
|
||||||
|
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
|
||||||
|
|
||||||
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||||
onIncludeListClick.invoke(isChecked)
|
onIncludeListClick.invoke(isChecked)
|
||||||
}
|
|
||||||
|
|
||||||
|
PrefManager.setVal(PrefName.PopularMangaList, isChecked)
|
||||||
|
}
|
||||||
if (ready.value == false)
|
if (ready.value == false)
|
||||||
ready.postValue(true)
|
ready.postValue(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var onIncludeListClick : ((Boolean)->Unit)
|
lateinit var onIncludeListClick: ((Boolean) -> Unit)
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
@@ -142,7 +145,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
binding.mangaTrendingViewPager.currentItem = binding.mangaTrendingViewPager.currentItem + 1
|
binding.mangaTrendingViewPager.currentItem =
|
||||||
|
binding.mangaTrendingViewPager.currentItem + 1
|
||||||
}
|
}
|
||||||
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
@@ -154,23 +158,30 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.mangaTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.mangaTrendingViewPager.layoutAnimation =
|
||||||
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.mangaListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.mangaTitleContainer.startAnimation(setSlideUp())
|
||||||
|
binding.mangaListContainer.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNovel(adaptor: MediaAdaptor) {
|
fun updateNovel(adaptor: MediaAdaptor) {
|
||||||
binding.mangaNovelProgressBar.visibility = View.GONE
|
binding.mangaNovelProgressBar.visibility = View.GONE
|
||||||
binding.mangaNovelRecyclerView.adapter = adaptor
|
binding.mangaNovelRecyclerView.adapter = adaptor
|
||||||
binding.mangaNovelRecyclerView.layoutManager =
|
binding.mangaNovelRecyclerView.layoutManager =
|
||||||
LinearLayoutManager(binding.mangaNovelRecyclerView.context, LinearLayoutManager.HORIZONTAL, false)
|
LinearLayoutManager(
|
||||||
|
binding.mangaNovelRecyclerView.context,
|
||||||
|
LinearLayoutManager.HORIZONTAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||||
|
|
||||||
binding.mangaNovel.visibility = View.VISIBLE
|
binding.mangaNovel.visibility = View.VISIBLE
|
||||||
binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
|
binding.mangaNovel.startAnimation(setSlideUp())
|
||||||
binding.mangaNovelRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
binding.mangaNovelRecyclerView.layoutAnimation =
|
||||||
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.mangaPopular.visibility = View.VISIBLE
|
binding.mangaPopular.visibility = View.VISIBLE
|
||||||
binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
|
binding.mangaPopular.startAnimation(setSlideUp())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAvatar() {
|
fun updateAvatar() {
|
||||||
@@ -180,5 +191,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.addCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.doOnAttach
|
import androidx.core.view.doOnAttach
|
||||||
@@ -16,54 +18,58 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.ZoomOutPageTransformer
|
import ani.dantotsu.ZoomOutPageTransformer
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
|
||||||
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
||||||
|
import ani.dantotsu.download.anime.OfflineAnimeFragment
|
||||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.isOnline
|
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.offline.OfflineFragment
|
import ani.dantotsu.offline.OfflineFragment
|
||||||
import ani.dantotsu.selectedOption
|
import ani.dantotsu.selectedOption
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
|
|
||||||
class NoInternet : AppCompatActivity() {
|
class NoInternet : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityNoInternetBinding
|
private lateinit var binding: ActivityNoInternetBinding
|
||||||
lateinit var bottomBar: AnimatedBottomBar
|
|
||||||
private var uiSettings = UserInterfaceSettings()
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
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) {
|
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 currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||||
backgroundDrawable.setColor(semiTransparentColor)
|
backgroundDrawable.setColor(semiTransparentColor)
|
||||||
_bottomBar.background = backgroundDrawable
|
bottomBar.background = backgroundDrawable
|
||||||
}
|
}
|
||||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||||
.getBoolean("colorOverflow", false)
|
|
||||||
if (!colorOverflow) {
|
|
||||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
|
||||||
|
|
||||||
|
|
||||||
|
var doubleBackToExitPressedOnce = false
|
||||||
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
|
if (doubleBackToExitPressedOnce) {
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
doubleBackToExitPressedOnce = true
|
||||||
|
snackString(this@NoInternet.getString(R.string.back_to_exit))
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed(
|
||||||
|
{ doubleBackToExitPressedOnce = false },
|
||||||
|
2000
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.root.doOnAttach {
|
binding.root.doOnAttach {
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
selectedOption = PrefManager.getVal(PrefName.DefaultStartUpTab)
|
||||||
selectedOption = uiSettings.defaultStartUpTab
|
|
||||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = navBarHeight
|
bottomMargin = navBarHeight
|
||||||
}
|
}
|
||||||
@@ -74,7 +80,7 @@ ThemeManager(this).applyTheme()
|
|||||||
val mainViewPager = binding.viewpager
|
val mainViewPager = binding.viewpager
|
||||||
mainViewPager.isUserInputEnabled = false
|
mainViewPager.isUserInputEnabled = false
|
||||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||||
navbar.setOnTabSelectListener(object :
|
navbar.setOnTabSelectListener(object :
|
||||||
AnimatedBottomBar.OnTabSelectListener {
|
AnimatedBottomBar.OnTabSelectListener {
|
||||||
override fun onTabSelected(
|
override fun onTabSelected(
|
||||||
@@ -100,12 +106,11 @@ ThemeManager(this).applyTheme()
|
|||||||
override fun getItemCount(): Int = 3
|
override fun getItemCount(): Int = 3
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
when (position) {
|
return when (position) {
|
||||||
0 -> return OfflineFragment()
|
0 -> OfflineAnimeFragment()
|
||||||
1 -> return OfflineFragment()
|
2 -> OfflineMangaFragment()
|
||||||
2 -> return OfflineMangaFragment()
|
else -> OfflineFragment()
|
||||||
}
|
}
|
||||||
return LoginFragment()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,16 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.EmptyAdapter
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityAuthorBinding
|
import ani.dantotsu.databinding.ActivityAuthorBinding
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.px
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -30,8 +35,8 @@ class AuthorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,24 @@ import android.annotation.SuppressLint
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityListBinding
|
import ani.dantotsu.databinding.ActivityListBinding
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||||
import ani.dantotsu.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 ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -34,8 +37,8 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityListBinding.inflate(layoutInflater)
|
binding = ActivityListBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +46,11 @@ ThemeManager(this).applyTheme()
|
|||||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||||
val primaryColor = typedValue.data
|
val primaryColor = typedValue.data
|
||||||
val typedValue2 = TypedValue()
|
val typedValue2 = TypedValue()
|
||||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue2, true)
|
theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorOnBackground,
|
||||||
|
typedValue2,
|
||||||
|
true
|
||||||
|
)
|
||||||
val titleTextColor = typedValue2.data
|
val titleTextColor = typedValue2.data
|
||||||
val typedValue3 = TypedValue()
|
val typedValue3 = TypedValue()
|
||||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||||
@@ -56,19 +63,25 @@ ThemeManager(this).applyTheme()
|
|||||||
window.navigationBarColor = primaryColor
|
window.navigationBarColor = primaryColor
|
||||||
binding.listTabLayout.setBackgroundColor(primaryColor)
|
binding.listTabLayout.setBackgroundColor(primaryColor)
|
||||||
binding.listAppBar.setBackgroundColor(primaryColor)
|
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||||
binding.listTitle.setTextColor(titleTextColor)
|
binding.listTitle.setTextColor(primaryTextColor)
|
||||||
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||||
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
|
||||||
if (!uiSettings.immersiveMode) {
|
|
||||||
this.window.statusBarColor =
|
this.window.statusBarColor =
|
||||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||||
binding.root.fitsSystemWindows = true
|
binding.root.fitsSystemWindows = true
|
||||||
|
|
||||||
}else{
|
} else {
|
||||||
binding.root.fitsSystemWindows = false
|
binding.root.fitsSystemWindows = false
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
window.setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
|
)
|
||||||
|
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = statusBarHeight
|
||||||
|
bottomMargin = navBarHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
@@ -79,14 +92,15 @@ ThemeManager(this).applyTheme()
|
|||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
||||||
}
|
}
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) { }
|
|
||||||
override fun onTabReselected(tab: TabLayout.Tab?) { }
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
model.getCalendar().observe(this) {
|
model.getCalendar().observe(this) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
binding.listProgressBar.visibility = View.GONE
|
binding.listProgressBar.visibility = View.GONE
|
||||||
binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true,this)
|
binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true, this)
|
||||||
val keys = it.keys.toList()
|
val keys = it.keys.toList()
|
||||||
val values = it.values.toList()
|
val values = it.values.toList()
|
||||||
val savedTab = this.selectedTabIdx
|
val savedTab = this.selectedTabIdx
|
||||||
|
|||||||
@@ -11,26 +11,23 @@ import androidx.core.util.Pair
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.databinding.ItemCharacterBinding
|
import ani.dantotsu.databinding.ItemCharacterBinding
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.setAnimation
|
import ani.dantotsu.setAnimation
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
class CharacterAdapter(
|
class CharacterAdapter(
|
||||||
private val characterList: ArrayList<Character>
|
private val characterList: ArrayList<Character>
|
||||||
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
|
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
|
||||||
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return CharacterViewHolder(binding)
|
return CharacterViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(binding.root.context, holder.binding.root, uiSettings)
|
setAnimation(binding.root.context, holder.binding.root)
|
||||||
val character = characterList[position]
|
val character = characterList[position]
|
||||||
binding.itemCompactRelation.text = character.role + " "
|
binding.itemCompactRelation.text = character.role + " "
|
||||||
binding.itemCompactImage.loadImage(character.image)
|
binding.itemCompactImage.loadImage(character.image)
|
||||||
@@ -38,16 +35,23 @@ class CharacterAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = characterList.size
|
override fun getItemCount(): Int = characterList.size
|
||||||
inner class CharacterViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class CharacterViewHolder(val binding: ItemCharacterBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
val char = characterList[bindingAdapterPosition]
|
val char = characterList[bindingAdapterPosition]
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
itemView.context,
|
itemView.context,
|
||||||
Intent(itemView.context, CharacterDetailsActivity::class.java).putExtra("character", char as Serializable),
|
Intent(
|
||||||
|
itemView.context,
|
||||||
|
CharacterDetailsActivity::class.java
|
||||||
|
).putExtra("character", char as Serializable),
|
||||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
itemView.context as Activity,
|
itemView.context as Activity,
|
||||||
Pair.create(binding.itemCompactImage, ViewCompat.getTransitionName(binding.itemCompactImage)!!),
|
Pair.create(
|
||||||
|
binding.itemCompactImage,
|
||||||
|
ViewCompat.getTransitionName(binding.itemCompactImage)!!
|
||||||
|
),
|
||||||
).toBundle()
|
).toBundle()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,19 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityCharacterBinding
|
import ani.dantotsu.databinding.ActivityCharacterBinding
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.px
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -31,20 +37,21 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
private val model: OtherDetailsViewModel by viewModels()
|
private val model: OtherDetailsViewModel by viewModels()
|
||||||
private lateinit var character: Character
|
private lateinit var character: Character
|
||||||
private var loaded = false
|
private var loaded = false
|
||||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status)
|
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.status)
|
||||||
|
|
||||||
val banner = if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
|
val banner =
|
||||||
|
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
|
||||||
|
|
||||||
banner.updateLayoutParams { height += statusBarHeight }
|
banner.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||||
@@ -61,7 +68,13 @@ ThemeManager(this).applyTheme()
|
|||||||
binding.characterTitle.text = character.name
|
binding.characterTitle.text = character.name
|
||||||
banner.loadImage(character.banner)
|
banner.loadImage(character.banner)
|
||||||
binding.characterCoverImage.loadImage(character.image)
|
binding.characterCoverImage.loadImage(character.image)
|
||||||
binding.characterCoverImage.setOnLongClickListener { ImageViewDialog.newInstance(this, character.name, character.image) }
|
binding.characterCoverImage.setOnLongClickListener {
|
||||||
|
ImageViewDialog.newInstance(
|
||||||
|
this,
|
||||||
|
character.name,
|
||||||
|
character.image
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
model.getCharacter().observe(this) {
|
model.getCharacter().observe(this) {
|
||||||
if (it != null && !loaded) {
|
if (it != null && !loaded) {
|
||||||
@@ -73,14 +86,15 @@ ThemeManager(this).applyTheme()
|
|||||||
val roles = character.roles
|
val roles = character.roles
|
||||||
if (roles != null) {
|
if (roles != null) {
|
||||||
val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true)
|
val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true)
|
||||||
val concatAdaptor = ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor)
|
val concatAdaptor =
|
||||||
|
ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor)
|
||||||
|
|
||||||
val gridSize = (screenWidth / 124f).toInt()
|
val gridSize = (screenWidth / 124f).toInt()
|
||||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
override fun getSpanSize(position: Int): Int {
|
override fun getSpanSize(position: Int): Int {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> gridSize
|
0 -> gridSize
|
||||||
else -> 1
|
else -> 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,16 +132,19 @@ ThemeManager(this).applyTheme()
|
|||||||
binding.characterCover.scaleY = 1f * cap
|
binding.characterCover.scaleY = 1f * cap
|
||||||
binding.characterCover.cardElevation = 32f * cap
|
binding.characterCover.cardElevation = 32f * cap
|
||||||
|
|
||||||
binding.characterCover.visibility = if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
binding.characterCover.visibility =
|
||||||
|
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||||
|
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
||||||
if (percentage >= percent && !isCollapsed) {
|
if (percentage >= percent && !isCollapsed) {
|
||||||
isCollapsed = true
|
isCollapsed = true
|
||||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
if (immersiveMode) this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.nav_bg)
|
||||||
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
||||||
}
|
}
|
||||||
if (percentage <= percent && isCollapsed) {
|
if (percentage <= percent && isCollapsed) {
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status)
|
if (immersiveMode) this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.status)
|
||||||
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
|||||||
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
|
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
|
||||||
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
|
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
|
||||||
val binding = ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return GenreViewHolder(binding)
|
return GenreViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,20 +24,22 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
|||||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val desc =
|
val desc =
|
||||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
||||||
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
||||||
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when(character.gender){
|
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||||
"Male" -> currActivity()!!.getString(R.string.male)
|
"Male" -> currActivity()!!.getString(R.string.male)
|
||||||
"Female" -> currActivity()!!.getString(R.string.female)
|
"Female" -> currActivity()!!.getString(R.string.female)
|
||||||
else -> character.gender
|
else -> character.gender
|
||||||
} else "") + "\n" + character.description
|
} else "") + "\n" + character.description
|
||||||
|
|
||||||
binding.characterDesc.isTextSelectable
|
binding.characterDesc.isTextSelectable
|
||||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(SpoilerPlugin()).build()
|
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||||
|
.usePlugin(SpoilerPlugin()).build()
|
||||||
markWon.setMarkdown(binding.characterDesc, desc)
|
markWon.setMarkdown(binding.characterDesc, desc)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,11 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -27,8 +27,8 @@ class GenreActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityGenreBinding.inflate(layoutInflater)
|
binding = ActivityGenreBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
@@ -50,10 +50,13 @@ ThemeManager(this).applyTheme()
|
|||||||
model.doneListener?.invoke()
|
model.doneListener?.invoke()
|
||||||
}
|
}
|
||||||
binding.mediaInfoGenresRecyclerView.adapter = adapter
|
binding.mediaInfoGenresRecyclerView.adapter = adapter
|
||||||
binding.mediaInfoGenresRecyclerView.layoutManager = GridLayoutManager(this, (screenWidth / 156f).toInt())
|
binding.mediaInfoGenresRecyclerView.layoutManager =
|
||||||
|
GridLayoutManager(this, (screenWidth / 156f).toInt())
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {
|
model.loadGenres(
|
||||||
|
Anilist.genres ?: loadLocalGenres() ?: arrayListOf()
|
||||||
|
) {
|
||||||
MainScope().launch {
|
MainScope().launch {
|
||||||
adapter.addGenre(it)
|
adapter.addGenre(it)
|
||||||
}
|
}
|
||||||
@@ -61,4 +64,14 @@ ThemeManager(this).applyTheme()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadLocalGenres(): ArrayList<String>? {
|
||||||
|
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||||
|
.toMutableList() as ArrayList<String>?
|
||||||
|
return if (genres.isNullOrEmpty()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
genres
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,8 @@ class GenreAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = genres.size
|
override fun getItemCount(): Int = genres.size
|
||||||
inner class GenreViewHolder(val binding: ItemGenreBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class GenreViewHolder(val binding: ItemGenreBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
@@ -48,15 +49,15 @@ class GenreAdapter(
|
|||||||
.putExtra("sortBy", Anilist.sortBy[2])
|
.putExtra("sortBy", Anilist.sortBy[2])
|
||||||
.putExtra("search", true)
|
.putExtra("search", true)
|
||||||
.also {
|
.also {
|
||||||
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
|
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
|
||||||
if (!Anilist.adult) Toast.makeText(
|
if (!Anilist.adult) Toast.makeText(
|
||||||
itemView.context,
|
itemView.context,
|
||||||
currActivity()?.getString(R.string.content_18),
|
currActivity()?.getString(R.string.content_18),
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
it.putExtra("hentai", true)
|
it.putExtra("hentai", true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.connections.anilist.api.MediaEdge
|
import ani.dantotsu.connections.anilist.api.MediaEdge
|
||||||
import ani.dantotsu.connections.anilist.api.MediaList
|
import ani.dantotsu.connections.anilist.api.MediaList
|
||||||
@@ -40,7 +41,7 @@ data class Media(
|
|||||||
var userUpdatedAt: Long? = null,
|
var userUpdatedAt: Long? = null,
|
||||||
var userStartedAt: FuzzyDate = FuzzyDate(),
|
var userStartedAt: FuzzyDate = FuzzyDate(),
|
||||||
var userCompletedAt: FuzzyDate = FuzzyDate(),
|
var userCompletedAt: FuzzyDate = FuzzyDate(),
|
||||||
var inCustomListsOf: MutableMap<String, Boolean>?= null,
|
var inCustomListsOf: MutableMap<String, Boolean>? = null,
|
||||||
var userFavOrder: Int? = null,
|
var userFavOrder: Int? = null,
|
||||||
|
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
@@ -69,7 +70,7 @@ data class Media(
|
|||||||
var shareLink: String? = null,
|
var shareLink: String? = null,
|
||||||
var selected: Selected? = null,
|
var selected: Selected? = null,
|
||||||
|
|
||||||
var idKitsu: String?=null,
|
var idKitsu: String? = null,
|
||||||
|
|
||||||
var cameFromContinue: Boolean = false
|
var cameFromContinue: Boolean = false
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
@@ -117,6 +118,20 @@ data class Media(
|
|||||||
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
|
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun emptyMedia() = Media(
|
||||||
|
id = 0,
|
||||||
|
name = "No media found",
|
||||||
|
nameRomaji = "No media found",
|
||||||
|
userPreferredName = "",
|
||||||
|
isAdult = false,
|
||||||
|
isFav = false,
|
||||||
|
isListPrivate = false,
|
||||||
|
userScore = 0,
|
||||||
|
userStatus = "",
|
||||||
|
format = "",
|
||||||
|
)
|
||||||
|
|
||||||
object MediaSingleton {
|
object MediaSingleton {
|
||||||
var media: Media? = null
|
var media: Media? = null
|
||||||
|
var bitmap: Bitmap? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.util.Pair
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -19,7 +26,8 @@ import ani.dantotsu.databinding.ItemMediaCompactBinding
|
|||||||
import ani.dantotsu.databinding.ItemMediaLargeBinding
|
import ani.dantotsu.databinding.ItemMediaLargeBinding
|
||||||
import ani.dantotsu.databinding.ItemMediaPageBinding
|
import ani.dantotsu.databinding.ItemMediaPageBinding
|
||||||
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
|
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.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
@@ -37,20 +45,40 @@ class MediaAdaptor(
|
|||||||
private val viewPager: ViewPager2? = null,
|
private val viewPager: ViewPager2? = null,
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
0 -> MediaViewHolder(ItemMediaCompactBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
0 -> MediaViewHolder(
|
||||||
1 -> MediaLargeViewHolder(ItemMediaLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
ItemMediaCompactBinding.inflate(
|
||||||
2 -> MediaPageViewHolder(ItemMediaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
LayoutInflater.from(parent.context),
|
||||||
3 -> MediaPageSmallViewHolder(
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
1 -> MediaLargeViewHolder(
|
||||||
|
ItemMediaLargeBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
2 -> MediaPageViewHolder(
|
||||||
|
ItemMediaPageBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
3 -> MediaPageSmallViewHolder(
|
||||||
ItemMediaPageSmallBinding.inflate(
|
ItemMediaPageSmallBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,14 +89,16 @@ class MediaAdaptor(
|
|||||||
when (type) {
|
when (type) {
|
||||||
0 -> {
|
0 -> {
|
||||||
val b = (holder as MediaViewHolder).binding
|
val b = (holder as MediaViewHolder).binding
|
||||||
setAnimation(activity, b.root, uiSettings)
|
setAnimation(activity, b.root)
|
||||||
val media = mediaList?.getOrNull(position)
|
val media = mediaList?.getOrNull(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
?: 0) else media.userScore) / 10.0).toString()
|
||||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||||
b.root.context,
|
b.root.context,
|
||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
@@ -100,29 +130,37 @@ class MediaAdaptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
val b = (holder as MediaLargeViewHolder).binding
|
val b = (holder as MediaLargeViewHolder).binding
|
||||||
setAnimation(activity, b.root, uiSettings)
|
setAnimation(activity, b.root)
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400)
|
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
|
||||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
?: 0) else media.userScore) / 10.0).toString()
|
||||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||||
b.root.context,
|
b.root.context,
|
||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
)
|
)
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||||
else currActivity()!!.getString(R.string.episode_singular)
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.episode_plural)
|
||||||
|
else currActivity()!!.getString(R.string.episode_singular)
|
||||||
b.itemCompactTotal.text =
|
b.itemCompactTotal.text =
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
?: "??").toString()) else (media.anime.totalEpisodes
|
||||||
|
?: "??").toString()
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.chapter_plural)
|
||||||
else currActivity()!!.getString(R.string.chapter_singular)
|
else currActivity()!!.getString(R.string.chapter_singular)
|
||||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||||
}
|
}
|
||||||
@@ -133,19 +171,22 @@ class MediaAdaptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> {
|
2 -> {
|
||||||
val b = (holder as MediaPageViewHolder).binding
|
val b = (holder as MediaPageViewHolder).binding
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
|
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
if (uiSettings.bannerAnimations)
|
if (bannerAnimations)
|
||||||
b.itemCompactBanner.setTransitionGenerator(
|
b.itemCompactBanner.setTransitionGenerator(
|
||||||
RandomTransitionGenerator(
|
RandomTransitionGenerator(
|
||||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed)) as Float)).toLong(),
|
||||||
AccelerateDecelerateInterpolator()
|
AccelerateDecelerateInterpolator()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
val banner =
|
||||||
|
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||||
val context = b.itemCompactBanner.context
|
val context = b.itemCompactBanner.context
|
||||||
if (!(context as Activity).isDestroyed)
|
if (!(context as Activity).isDestroyed)
|
||||||
Glide.with(context as Context)
|
Glide.with(context as Context)
|
||||||
@@ -153,22 +194,29 @@ class MediaAdaptor(
|
|||||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||||
.into(banner)
|
.into(banner)
|
||||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
?: 0) else media.userScore) / 10.0).toString()
|
||||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||||
b.root.context,
|
b.root.context,
|
||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
)
|
)
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.episode_plural)
|
||||||
else currActivity()!!.getString(R.string.episode_singular)
|
else currActivity()!!.getString(R.string.episode_singular)
|
||||||
b.itemCompactTotal.text =
|
b.itemCompactTotal.text =
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
?: "??").toString()) else (media.anime.totalEpisodes
|
||||||
|
?: "??").toString()
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
b.itemTotal.text =" " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.chapter_plural)
|
||||||
else currActivity()!!.getString(R.string.chapter_singular)
|
else currActivity()!!.getString(R.string.chapter_singular)
|
||||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||||
}
|
}
|
||||||
@@ -180,19 +228,22 @@ class MediaAdaptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
3 -> {
|
3 -> {
|
||||||
val b = (holder as MediaPageSmallViewHolder).binding
|
val b = (holder as MediaPageSmallViewHolder).binding
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
|
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
if (uiSettings.bannerAnimations)
|
if (bannerAnimations)
|
||||||
b.itemCompactBanner.setTransitionGenerator(
|
b.itemCompactBanner.setTransitionGenerator(
|
||||||
RandomTransitionGenerator(
|
RandomTransitionGenerator(
|
||||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
|
||||||
AccelerateDecelerateInterpolator()
|
AccelerateDecelerateInterpolator()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
val banner =
|
||||||
|
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||||
val context = b.itemCompactBanner.context
|
val context = b.itemCompactBanner.context
|
||||||
if (!(context as Activity).isDestroyed)
|
if (!(context as Activity).isDestroyed)
|
||||||
Glide.with(context as Context)
|
Glide.with(context as Context)
|
||||||
@@ -200,10 +251,12 @@ class MediaAdaptor(
|
|||||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||||
.into(banner)
|
.into(banner)
|
||||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
?: 0) else media.userScore) / 10.0).toString()
|
||||||
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
|
||||||
b.root.context,
|
b.root.context,
|
||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
@@ -218,13 +271,18 @@ class MediaAdaptor(
|
|||||||
}
|
}
|
||||||
b.itemCompactStatus.text = media.status ?: ""
|
b.itemCompactStatus.text = media.status ?: ""
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.episode_plural)
|
||||||
else currActivity()!!.getString(R.string.episode_singular)
|
else currActivity()!!.getString(R.string.episode_singular)
|
||||||
b.itemCompactTotal.text =
|
b.itemCompactTotal.text =
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||||
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
|
?: "??").toString()) else (media.anime.totalEpisodes
|
||||||
|
?: "??").toString()
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
|
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||||
|
?: 0) != 1
|
||||||
|
) currActivity()!!.getString(R.string.chapter_plural)
|
||||||
else currActivity()!!.getString(R.string.chapter_singular)
|
else currActivity()!!.getString(R.string.chapter_singular)
|
||||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||||
}
|
}
|
||||||
@@ -245,61 +303,161 @@ class MediaAdaptor(
|
|||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) : RecyclerView.ViewHolder(binding.root) {
|
fun randomOptionClick() {
|
||||||
|
val media = if (!mediaList.isNullOrEmpty()) {
|
||||||
|
mediaList.random()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
media?.let {
|
||||||
|
val index = mediaList?.indexOf(it) ?: -1
|
||||||
|
clicked(index, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
if (matchParent) itemView.updateLayoutParams { width = -1 }
|
if (matchParent) itemView.updateLayoutParams { width = -1 }
|
||||||
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
itemView.setSafeOnClickListener {
|
||||||
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
itemView.setSafeOnClickListener {
|
||||||
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
binding.itemCompactImage.setSafeOnClickListener {
|
||||||
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
itemView.setOnTouchListener { _, _ -> true }
|
itemView.setOnTouchListener { _, _ -> true }
|
||||||
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
binding.itemCompactImage.setSafeOnClickListener {
|
||||||
binding.itemCompactTitleContainer.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.itemCompactTitleContainer.setSafeOnClickListener {
|
||||||
|
clicked(
|
||||||
|
bindingAdapterPosition,
|
||||||
|
binding.itemCompactImage,
|
||||||
|
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
itemView.setOnTouchListener { _, _ -> true }
|
itemView.setOnTouchListener { _, _ -> true }
|
||||||
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clicked(position: Int) {
|
fun clicked(position: Int, itemCompactImage: ImageView?, bitmap: Bitmap? = null) {
|
||||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
|
if (bitmap != null) MediaSingleton.bitmap = bitmap
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
activity,
|
activity,
|
||||||
Intent(activity, MediaDetailsActivity::class.java).putExtra(
|
Intent(activity, MediaDetailsActivity::class.java).putExtra(
|
||||||
"media",
|
"media",
|
||||||
media as Serializable
|
media as Serializable
|
||||||
), null
|
),
|
||||||
|
if (itemCompactImage != null) {
|
||||||
|
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
|
activity,
|
||||||
|
itemCompactImage,
|
||||||
|
ViewCompat.getTransitionName(itemCompactImage)!!
|
||||||
|
).toBundle()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun longClicked(position: Int): Boolean {
|
fun longClicked(position: Int): Boolean {
|
||||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||||
val media = mediaList?.get(position) ?: return false
|
val media = mediaList?.get(position) ?: return false
|
||||||
if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
|
if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
|
||||||
MediaListDialogSmallFragment.newInstance(media).show(activity.supportFragmentManager, "list")
|
MediaListDialogSmallFragment.newInstance(media)
|
||||||
|
.show(activity.supportFragmentManager, "list")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getBitmapFromImageView(imageView: ImageView): Bitmap? {
|
||||||
|
val drawable = imageView.drawable ?: return null
|
||||||
|
|
||||||
|
// If the drawable is a BitmapDrawable, then just get the bitmap
|
||||||
|
if (drawable is BitmapDrawable) {
|
||||||
|
return drawable.bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a bitmap with the same dimensions as the drawable
|
||||||
|
val bitmap = Bitmap.createBitmap(
|
||||||
|
drawable.intrinsicWidth,
|
||||||
|
drawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw the drawable onto the bitmap
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resizeBitmap(source: Bitmap?, maxDimension: Int): Bitmap? {
|
||||||
|
if (source == null) return null
|
||||||
|
val width = source.width
|
||||||
|
val height = source.height
|
||||||
|
val newWidth: Int
|
||||||
|
val newHeight: Int
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
newWidth = maxDimension
|
||||||
|
newHeight = (height * (maxDimension.toFloat() / width)).toInt()
|
||||||
|
} else {
|
||||||
|
newHeight = maxDimension
|
||||||
|
newWidth = (width * (maxDimension.toFloat() / height)).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bitmap.createScaledBitmap(source, newWidth, newHeight, true)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -31,24 +31,22 @@ import ani.dantotsu.R
|
|||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.ZoomOutPageTransformer
|
import ani.dantotsu.ZoomOutPageTransformer
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
|
||||||
import ani.dantotsu.copyToClipboard
|
import ani.dantotsu.copyToClipboard
|
||||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment
|
import ani.dantotsu.media.manga.MangaReadFragment
|
||||||
import ani.dantotsu.navBarHeight
|
|
||||||
import ani.dantotsu.media.novel.NovelReadFragment
|
import ani.dantotsu.media.novel.NovelReadFragment
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.saveData
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
@@ -65,7 +63,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
private val scope = lifecycleScope
|
private val scope = lifecycleScope
|
||||||
private val model: MediaDetailsViewModel by viewModels()
|
private val model: MediaDetailsViewModel by viewModels()
|
||||||
private lateinit var tabLayout: NavigationBarView
|
private lateinit var tabLayout: NavigationBarView
|
||||||
private lateinit var uiSettings: UserInterfaceSettings
|
|
||||||
var selected = 0
|
var selected = 0
|
||||||
var anime = true
|
var anime = true
|
||||||
private var adult = false
|
private var adult = false
|
||||||
@@ -73,8 +70,16 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||||
ThemeManager(this).applyTheme()
|
if (media.name == "No media found") {
|
||||||
|
snackString(media.name)
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mediaSingleton = null
|
||||||
|
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||||
|
MediaSingleton.bitmap = null
|
||||||
|
|
||||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||||
@@ -82,13 +87,11 @@ ThemeManager(this).applyTheme()
|
|||||||
//Ui init
|
//Ui init
|
||||||
|
|
||||||
initActivity(this)
|
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.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||||
|
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||||
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
||||||
|
|
||||||
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -104,22 +107,23 @@ ThemeManager(this).applyTheme()
|
|||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiSettings.bannerAnimations) {
|
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||||
|
if (bannerAnimations) {
|
||||||
val adi = AccelerateDecelerateInterpolator()
|
val adi = AccelerateDecelerateInterpolator()
|
||||||
val generator = RandomTransitionGenerator(
|
val generator = RandomTransitionGenerator(
|
||||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
|
||||||
adi
|
adi
|
||||||
)
|
)
|
||||||
binding.mediaBanner.setTransitionGenerator(generator)
|
binding.mediaBanner.setTransitionGenerator(generator)
|
||||||
}
|
}
|
||||||
val banner =
|
val banner =
|
||||||
if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||||
val viewPager = binding.mediaViewPager
|
val viewPager = binding.mediaViewPager
|
||||||
tabLayout = binding.mediaTab as NavigationBarView
|
tabLayout = binding.mediaTab as NavigationBarView
|
||||||
viewPager.isUserInputEnabled = false
|
viewPager.isUserInputEnabled = false
|
||||||
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||||
|
|
||||||
|
|
||||||
var media: Media = intent.getSerialized("media") ?: return
|
|
||||||
val isDownload = intent.getBooleanExtra("download", false)
|
val isDownload = intent.getBooleanExtra("download", false)
|
||||||
media.selected = model.loadSelected(media, isDownload)
|
media.selected = model.loadSelected(media, isDownload)
|
||||||
|
|
||||||
@@ -134,7 +138,7 @@ ThemeManager(this).applyTheme()
|
|||||||
banner.loadImage(media.banner ?: media.cover, 400)
|
banner.loadImage(media.banner ?: media.cover, 400)
|
||||||
val gestureDetector = GestureDetector(this, object : GesturesListener() {
|
val gestureDetector = GestureDetector(this, object : GesturesListener() {
|
||||||
override fun onDoubleClick(event: MotionEvent) {
|
override fun onDoubleClick(event: MotionEvent) {
|
||||||
if (!uiSettings.bannerAnimations)
|
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
|
||||||
snackString(getString(R.string.enable_banner_animations))
|
snackString(getString(R.string.enable_banner_animations))
|
||||||
else {
|
else {
|
||||||
binding.mediaBanner.restart()
|
binding.mediaBanner.restart()
|
||||||
@@ -152,7 +156,12 @@ ThemeManager(this).applyTheme()
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
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 {
|
binding.mediaTitle.setOnLongClickListener {
|
||||||
copyToClipboard(media.userPreferredName)
|
copyToClipboard(media.userPreferredName)
|
||||||
true
|
true
|
||||||
@@ -271,7 +280,10 @@ ThemeManager(this).applyTheme()
|
|||||||
} else snackString(getString(R.string.please_login_anilist))
|
} else snackString(getString(R.string.please_login_anilist))
|
||||||
}
|
}
|
||||||
binding.mediaAddToList.setOnLongClickListener {
|
binding.mediaAddToList.setOnLongClickListener {
|
||||||
saveData("${media.id}_progressDialog", true)
|
PrefManager.setCustomVal(
|
||||||
|
"${media.id}_progressDialog",
|
||||||
|
true,
|
||||||
|
)
|
||||||
snackString(getString(R.string.auto_update_reset))
|
snackString(getString(R.string.auto_update_reset))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -303,7 +315,6 @@ ThemeManager(this).applyTheme()
|
|||||||
}
|
}
|
||||||
|
|
||||||
adult = media.isAdult
|
adult = media.isAdult
|
||||||
|
|
||||||
tabLayout.menu.clear()
|
tabLayout.menu.clear()
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
viewPager.adapter =
|
viewPager.adapter =
|
||||||
@@ -315,7 +326,11 @@ ThemeManager(this).applyTheme()
|
|||||||
lifecycle,
|
lifecycle,
|
||||||
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
|
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
|
anime = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +344,7 @@ ThemeManager(this).applyTheme()
|
|||||||
viewPager.setCurrentItem(selected, false)
|
viewPager.setCurrentItem(selected, false)
|
||||||
val sel = model.loadSelected(media, isDownload)
|
val sel = model.loadSelected(media, isDownload)
|
||||||
sel.window = selected
|
sel.window = selected
|
||||||
model.saveSelected(media.id, sel, this)
|
model.saveSelected(media.id, sel)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +353,7 @@ ThemeManager(this).applyTheme()
|
|||||||
viewPager.setCurrentItem(selected, false)
|
viewPager.setCurrentItem(selected, false)
|
||||||
|
|
||||||
if (model.continueMedia == null && media.cameFromContinue) {
|
if (model.continueMedia == null && media.cameFromContinue) {
|
||||||
model.continueMedia = loadData("continue_media") ?: true
|
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
||||||
selected = 1
|
selected = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,7 +394,9 @@ ThemeManager(this).applyTheme()
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
tabLayout.selectedItemId = idFromSelect()
|
if (this::tabLayout.isInitialized) {
|
||||||
|
tabLayout.selectedItemId = idFromSelect()
|
||||||
|
}
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +438,7 @@ ThemeManager(this).applyTheme()
|
|||||||
|
|
||||||
binding.mediaCover.visibility =
|
binding.mediaCover.visibility =
|
||||||
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
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()
|
val typedValue = TypedValue()
|
||||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||||
com.google.android.material.R.attr.colorSecondary,
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
@@ -440,7 +457,6 @@ ThemeManager(this).applyTheme()
|
|||||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
|
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
|
||||||
.setDuration(duration).start()
|
.setDuration(duration).start()
|
||||||
binding.mediaBanner.pause()
|
binding.mediaBanner.pause()
|
||||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
|
||||||
}
|
}
|
||||||
if (percentage <= percent && isCollapsed) {
|
if (percentage <= percent && isCollapsed) {
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
@@ -452,8 +468,7 @@ ThemeManager(this).applyTheme()
|
|||||||
.start()
|
.start()
|
||||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
|
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
|
||||||
.setDuration(duration).start()
|
.setDuration(duration).start()
|
||||||
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.mediaBanner.resume()
|
||||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
|
||||||
}
|
}
|
||||||
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
|
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
|
||||||
false
|
false
|
||||||
@@ -477,9 +492,6 @@ ThemeManager(this).applyTheme()
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
enabled(true)
|
enabled(true)
|
||||||
scope.launch {
|
|
||||||
clicked()
|
|
||||||
}
|
|
||||||
image.setOnClickListener {
|
image.setOnClickListener {
|
||||||
if (pressable && !disabled) {
|
if (pressable && !disabled) {
|
||||||
pressable = false
|
pressable = false
|
||||||
@@ -530,5 +542,9 @@ ThemeManager(this).applyTheme()
|
|||||||
image.alpha = if (disabled) 0.33f else 1f
|
image.alpha = if (disabled) 0.33f else 1f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var mediaSingleton: Media? = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +1,63 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Environment
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import ani.dantotsu.FileUrl
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.media.anime.Episode
|
import ani.dantotsu.media.anime.Episode
|
||||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||||
import ani.dantotsu.loadData
|
|
||||||
import ani.dantotsu.logger
|
|
||||||
import ani.dantotsu.media.manga.MangaChapter
|
import ani.dantotsu.media.manga.MangaChapter
|
||||||
import ani.dantotsu.others.AniSkip
|
import ani.dantotsu.others.AniSkip
|
||||||
import ani.dantotsu.others.Jikan
|
import ani.dantotsu.others.Jikan
|
||||||
import ani.dantotsu.others.Kitsu
|
import ani.dantotsu.others.Kitsu
|
||||||
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.Book
|
import ani.dantotsu.parsers.Book
|
||||||
import ani.dantotsu.parsers.MangaImage
|
import ani.dantotsu.parsers.MangaImage
|
||||||
import ani.dantotsu.parsers.MangaReadSources
|
import ani.dantotsu.parsers.MangaReadSources
|
||||||
|
import ani.dantotsu.parsers.MangaSources
|
||||||
import ani.dantotsu.parsers.NovelSources
|
import ani.dantotsu.parsers.NovelSources
|
||||||
import ani.dantotsu.parsers.ShowResponse
|
import ani.dantotsu.parsers.ShowResponse
|
||||||
import ani.dantotsu.parsers.VideoExtractor
|
import ani.dantotsu.parsers.VideoExtractor
|
||||||
import ani.dantotsu.parsers.WatchSources
|
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.snackString
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import ani.dantotsu.currContext
|
|
||||||
import ani.dantotsu.R
|
|
||||||
import ani.dantotsu.download.Download
|
|
||||||
import ani.dantotsu.download.DownloadsManager
|
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
|
||||||
import ani.dantotsu.parsers.AniyomiAdapter
|
|
||||||
import ani.dantotsu.parsers.DynamicMangaParser
|
|
||||||
import ani.dantotsu.parsers.HAnimeSources
|
|
||||||
import ani.dantotsu.parsers.HMangaSources
|
|
||||||
import ani.dantotsu.parsers.MangaSources
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class MediaDetailsViewModel : ViewModel() {
|
class MediaDetailsViewModel : ViewModel() {
|
||||||
val scrolledToTop = MutableLiveData(true)
|
val scrolledToTop = MutableLiveData(true)
|
||||||
|
|
||||||
fun saveSelected(id: Int, data: Selected, activity: Activity? = null) {
|
fun saveSelected(id: Int, data: Selected) {
|
||||||
saveData("$id-select", data, activity)
|
PrefManager.setCustomVal("Selected-$id", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
|
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
|
||||||
val sharedPreferences = Injekt.get<SharedPreferences>()
|
val data =
|
||||||
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
|
PrefManager.getNullableCustomVal("Selected-${media.id}", null, Selected::class.java)
|
||||||
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
|
?: Selected().let {
|
||||||
true ->sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
|
it.sourceIndex = 0
|
||||||
else ->sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
|
it.preferDub = PrefManager.getVal(PrefName.SettingsPreferDub)
|
||||||
}
|
saveSelected(media.id, it)
|
||||||
it.preferDub = loadData("settings_prefer_dub") ?: false
|
it
|
||||||
saveSelected(media.id, it)
|
}
|
||||||
it
|
|
||||||
}
|
|
||||||
if (isDownload) {
|
if (isDownload) {
|
||||||
data.sourceIndex = when (media.anime != null) {
|
data.sourceIndex = if (media.anime != null) {
|
||||||
true -> AnimeSources.list.size - 1
|
AnimeSources.list.size - 1
|
||||||
else -> MangaSources.list.size - 1
|
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
||||||
|
MangaSources.list.size - 1
|
||||||
|
} else {
|
||||||
|
NovelSources.list.size - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
@@ -81,7 +66,9 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
fun loadSelectedStringLocation(sourceName: String): Int {
|
fun loadSelectedStringLocation(sourceName: String): Int {
|
||||||
//find the location of the source in the list
|
//find the location of the source in the list
|
||||||
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
||||||
if (location == -1) {location = 0}
|
if (location == -1) {
|
||||||
|
location = 0
|
||||||
|
}
|
||||||
return location
|
return location
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +93,9 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
|
|
||||||
|
|
||||||
//Anime
|
//Anime
|
||||||
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null)
|
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> =
|
||||||
|
MutableLiveData<Map<String, Episode>>(null)
|
||||||
|
|
||||||
fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes
|
fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes
|
||||||
suspend fun loadKitsuEpisodes(s: Media) {
|
suspend fun loadKitsuEpisodes(s: Media) {
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
@@ -114,7 +103,9 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null)
|
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> =
|
||||||
|
MutableLiveData<Map<String, Episode>>(null)
|
||||||
|
|
||||||
fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes
|
fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes
|
||||||
suspend fun loadFillerEpisodes(s: Media) {
|
suspend fun loadFillerEpisodes(s: Media) {
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
@@ -145,7 +136,8 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
|
|
||||||
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
|
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
|
||||||
watchSources?.saveResponse(i, id, source)
|
watchSources?.saveResponse(i, id, source)
|
||||||
epsLoaded[i] = watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
|
epsLoaded[i] =
|
||||||
|
watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
|
||||||
episodes.postValue(epsLoaded)
|
episodes.postValue(epsLoaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +176,12 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
|
|
||||||
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
|
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
|
||||||
private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf()
|
private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf()
|
||||||
suspend fun loadTimeStamps(malId: Int?, episodeNum: Int?, duration: Long, useProxyForTimeStamps: Boolean) {
|
suspend fun loadTimeStamps(
|
||||||
|
malId: Int?,
|
||||||
|
episodeNum: Int?,
|
||||||
|
duration: Long,
|
||||||
|
useProxyForTimeStamps: Boolean
|
||||||
|
) {
|
||||||
malId ?: return
|
malId ?: return
|
||||||
episodeNum ?: return
|
episodeNum ?: return
|
||||||
if (timeStampsMap.containsKey(episodeNum))
|
if (timeStampsMap.containsKey(episodeNum))
|
||||||
@@ -194,7 +191,11 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
timeStamps.postValue(result)
|
timeStamps.postValue(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadEpisodeSingleVideo(ep: Episode, selected: Selected, post: Boolean = true): Boolean {
|
suspend fun loadEpisodeSingleVideo(
|
||||||
|
ep: Episode,
|
||||||
|
selected: Selected,
|
||||||
|
post: Boolean = true
|
||||||
|
): Boolean {
|
||||||
if (ep.extractors.isNullOrEmpty()) {
|
if (ep.extractors.isNullOrEmpty()) {
|
||||||
|
|
||||||
val server = selected.server ?: return false
|
val server = selected.server ?: return false
|
||||||
@@ -204,8 +205,10 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
selected.sourceIndex = selected.sourceIndex
|
selected.sourceIndex = selected.sourceIndex
|
||||||
if (!post && !it.allowsPreloading) null
|
if (!post && !it.allowsPreloading) null
|
||||||
else ep.sEpisode?.let { it1 ->
|
else ep.sEpisode?.let { it1 ->
|
||||||
it.loadSingleVideoServer(server, link, ep.extra,
|
it.loadSingleVideoServer(
|
||||||
it1, post)
|
server, link, ep.extra,
|
||||||
|
it1, post
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} ?: return false)
|
} ?: return false)
|
||||||
ep.allStreams = false
|
ep.allStreams = false
|
||||||
@@ -228,7 +231,14 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val epChanged = MutableLiveData(true)
|
val epChanged = MutableLiveData(true)
|
||||||
fun onEpisodeClick(media: Media, i: String, manager: FragmentManager, launch: Boolean = true, prevEp: String? = null) {
|
fun onEpisodeClick(
|
||||||
|
media: Media,
|
||||||
|
i: String,
|
||||||
|
manager: FragmentManager,
|
||||||
|
launch: Boolean = true,
|
||||||
|
prevEp: String? = null,
|
||||||
|
isDownload: Boolean = false
|
||||||
|
) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
|
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
|
||||||
if (media.anime?.episodes?.get(i) != null) {
|
if (media.anime?.episodes?.get(i) != null) {
|
||||||
@@ -238,23 +248,32 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
media.selected = this.loadSelected(media)
|
media.selected = this.loadSelected(media)
|
||||||
val selector = SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
|
val selector =
|
||||||
|
SelectorDialogFragment.newInstance(
|
||||||
|
media.selected!!.server,
|
||||||
|
launch,
|
||||||
|
prevEp,
|
||||||
|
isDownload
|
||||||
|
)
|
||||||
selector.show(manager, "dialog")
|
selector.show(manager, "dialog")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Manga
|
//Manga
|
||||||
var mangaReadSources: MangaReadSources? = null
|
var mangaReadSources: MangaReadSources? = null
|
||||||
|
|
||||||
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
|
private val mangaChapters =
|
||||||
|
MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
|
||||||
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
|
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
|
||||||
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters
|
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> =
|
||||||
|
mangaChapters
|
||||||
|
|
||||||
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
||||||
logger("Loading Manga Chapters : $mangaLoaded")
|
logger("Loading Manga Chapters : $mangaLoaded")
|
||||||
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
||||||
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
mangaLoaded[i] =
|
||||||
|
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||||
}
|
}
|
||||||
mangaChapters.postValue(mangaLoaded)
|
mangaChapters.postValue(mangaLoaded)
|
||||||
}
|
}
|
||||||
@@ -269,31 +288,17 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
|
|
||||||
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
||||||
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
||||||
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, series: String, post: Boolean = true): Boolean {
|
suspend fun loadMangaChapterImages(
|
||||||
//check if the chapter has been downloaded already
|
chapter: MangaChapter,
|
||||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
selected: Selected,
|
||||||
if(downloadsManager.mangaDownloads.contains(Download(series, chapter.title!!, Download.Type.MANGA))) {
|
series: String,
|
||||||
val download = downloadsManager.mangaDownloads.find { it.title == series && it.chapter == chapter.title!! } ?: return false
|
post: Boolean = true
|
||||||
//look in the downloads folder for the chapter and add all the numerically named images to the chapter
|
): Boolean {
|
||||||
val directory = File(
|
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/Manga/$series/${chapter.title!!}"
|
|
||||||
)
|
|
||||||
val images = mutableListOf<MangaImage>()
|
|
||||||
directory.listFiles()?.forEach {
|
|
||||||
if (it.nameWithoutExtension.toIntOrNull() != null) {
|
|
||||||
images.add(MangaImage(FileUrl(it.absolutePath), false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//sort the images by name
|
|
||||||
images.sortBy { it.url.url }
|
|
||||||
chapter.addImages(images)
|
|
||||||
if (post) mangaChapter.postValue(chapter)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return tryWithSuspend(true) {
|
return tryWithSuspend(true) {
|
||||||
chapter.addImages(
|
chapter.addImages(
|
||||||
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
|
mangaReadSources?.get(selected.sourceIndex)
|
||||||
|
?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
|
||||||
)
|
)
|
||||||
if (post) mangaChapter.postValue(chapter)
|
if (post) mangaChapter.postValue(chapter)
|
||||||
true
|
true
|
||||||
@@ -301,13 +306,15 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? {
|
fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? {
|
||||||
return if (mangaImage.useTransformation) mangaReadSources?.get(source)?.getTransformation() else null
|
return if (mangaImage.useTransformation) mangaReadSources?.get(source)
|
||||||
|
?.getTransformation() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
val novelSources = NovelSources
|
val novelSources = NovelSources
|
||||||
val novelResponses = MutableLiveData<List<ShowResponse>>(null)
|
val novelResponses = MutableLiveData<List<ShowResponse>>(null)
|
||||||
suspend fun searchNovels(query: String, i: Int) {
|
suspend fun searchNovels(query: String, i: Int) {
|
||||||
val source = novelSources[i]
|
val position = if (i >= novelSources.list.size) 0 else i
|
||||||
|
val source = novelSources[position]
|
||||||
tryWithSuspend(post = true) {
|
tryWithSuspend(post = true) {
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
novelResponses.postValue(source.search(query))
|
novelResponses.postValue(source.search(query))
|
||||||
@@ -316,7 +323,7 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun autoSearchNovels(media: Media) {
|
suspend fun autoSearchNovels(media: Media) {
|
||||||
val source = novelSources[media.selected?.sourceIndex?:0]
|
val source = novelSources[media.selected?.sourceIndex ?: 0]
|
||||||
tryWithSuspend(post = true) {
|
tryWithSuspend(post = true) {
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
novelResponses.postValue(source.sortedSearch(media))
|
novelResponses.postValue(source.sortedSearch(media))
|
||||||
@@ -327,7 +334,9 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
val book: MutableLiveData<Book> = MutableLiveData(null)
|
val book: MutableLiveData<Book> = MutableLiveData(null)
|
||||||
suspend fun loadBook(novel: ShowResponse, i: Int) {
|
suspend fun loadBook(novel: ShowResponse, i: Int) {
|
||||||
tryWithSuspend {
|
tryWithSuspend {
|
||||||
book.postValue(novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend)
|
book.postValue(
|
||||||
|
novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import ani.dantotsu.*
|
|||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||||
import ani.dantotsu.databinding.*
|
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.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -43,7 +45,11 @@ class MediaInfoFragment : Fragment() {
|
|||||||
private var type = "ANIME"
|
private var type = "ANIME"
|
||||||
private val genreModel: GenresViewModel by activityViewModels()
|
private val genreModel: GenresViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
|
_binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -55,12 +61,13 @@ class MediaInfoFragment : Fragment() {
|
|||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val model: MediaDetailsViewModel by activityViewModels()
|
val model: MediaDetailsViewModel by activityViewModels()
|
||||||
|
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||||
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
||||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||||
|
|
||||||
model.scrolledToTop.observe(viewLifecycleOwner){
|
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||||
if(it) binding.mediaInfoScroll.scrollTo(0,0)
|
if (it) binding.mediaInfoScroll.scrollTo(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
@@ -68,56 +75,85 @@ class MediaInfoFragment : Fragment() {
|
|||||||
loaded = true
|
loaded = true
|
||||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||||
binding.mediaInfoContainer.visibility = View.VISIBLE
|
binding.mediaInfoContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoName.text = "\t\t\t" + (media.name?:media.nameRomaji)
|
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
|
||||||
binding.mediaInfoName.setOnLongClickListener {
|
binding.mediaInfoName.setOnLongClickListener {
|
||||||
copyToClipboard(media.name?:media.nameRomaji)
|
copyToClipboard(media.name ?: media.nameRomaji)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = View.VISIBLE
|
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
|
||||||
|
View.VISIBLE
|
||||||
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
||||||
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
||||||
copyToClipboard(media.nameRomaji)
|
copyToClipboard(media.nameRomaji)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
binding.mediaInfoMeanScore.text = if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
|
binding.mediaInfoMeanScore.text =
|
||||||
|
if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
|
||||||
binding.mediaInfoStatus.text = media.status
|
binding.mediaInfoStatus.text = media.status
|
||||||
binding.mediaInfoFormat.text = media.format
|
binding.mediaInfoFormat.text = media.format
|
||||||
binding.mediaInfoSource.text = media.source
|
binding.mediaInfoSource.text = media.source
|
||||||
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
||||||
binding.mediaInfoEnd.text =media.endDate?.toString() ?: "??"
|
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
binding.mediaInfoDuration.text =
|
val episodeDuration = media.anime.episodeDuration
|
||||||
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
|
|
||||||
|
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.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoSeason.text =
|
binding.mediaInfoSeason.text =
|
||||||
(media.anime.season ?: "??")+ " " + (media.anime.seasonYear ?: "??")
|
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
|
||||||
if (media.anime.mainStudio != null) {
|
if (media.anime.mainStudio != null) {
|
||||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
if (!offline) {
|
||||||
ContextCompat.startActivity(
|
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||||
requireActivity(),
|
ContextCompat.startActivity(
|
||||||
Intent(activity, StudioActivity::class.java).putExtra(
|
requireActivity(),
|
||||||
"studio",
|
Intent(activity, StudioActivity::class.java).putExtra(
|
||||||
media.anime.mainStudio!! as Serializable
|
"studio",
|
||||||
),
|
media.anime.mainStudio!! as Serializable
|
||||||
null
|
),
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (media.anime.author != null) {
|
if (media.anime.author != null) {
|
||||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoAuthor.text = media.anime.author!!.name
|
binding.mediaInfoAuthor.text = media.anime.author!!.name
|
||||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
if (!offline) {
|
||||||
ContextCompat.startActivity(
|
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||||
requireActivity(),
|
ContextCompat.startActivity(
|
||||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
requireActivity(),
|
||||||
"author",
|
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||||
media.anime.author!! as Serializable
|
"author",
|
||||||
),
|
media.anime.author!! as Serializable
|
||||||
null
|
),
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||||
@@ -131,15 +167,17 @@ class MediaInfoFragment : Fragment() {
|
|||||||
if (media.manga.author != null) {
|
if (media.manga.author != null) {
|
||||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoAuthor.text = media.manga.author!!.name
|
binding.mediaInfoAuthor.text = media.manga.author!!.name
|
||||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
if (!offline) {
|
||||||
ContextCompat.startActivity(
|
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||||
requireActivity(),
|
ContextCompat.startActivity(
|
||||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
requireActivity(),
|
||||||
"author",
|
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||||
media.manga.author!! as Serializable
|
"author",
|
||||||
),
|
media.manga.author!! as Serializable
|
||||||
null
|
),
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +221,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.trailer != null) {
|
if (media.trailer != null && !offline) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
class MyChrome : WebChromeClient() {
|
class MyChrome : WebChromeClient() {
|
||||||
private var mCustomView: View? = null
|
private var mCustomView: View? = null
|
||||||
@@ -237,7 +275,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) {
|
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty()) && !offline) {
|
||||||
val markWon = Markwon.builder(requireContext())
|
val markWon = Markwon.builder(requireContext())
|
||||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||||
|
|
||||||
@@ -246,7 +284,12 @@ class MediaInfoFragment : Fragment() {
|
|||||||
val end = a.indexOf('"', first).let { if (it != -1) it else return a }
|
val end = a.indexOf('"', first).let { if (it != -1) it else return a }
|
||||||
val name = a.subSequence(first, end).toString()
|
val name = a.subSequence(first, end).toString()
|
||||||
return "${a.subSequence(0, first)}" +
|
return "${a.subSequence(0, first)}" +
|
||||||
"[$name](https://www.youtube.com/results?search_query=${URLEncoder.encode(name, "utf-8")})" +
|
"[$name](https://www.youtube.com/results?search_query=${
|
||||||
|
URLEncoder.encode(
|
||||||
|
name,
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
})" +
|
||||||
"${a.subSequence(end, a.length)}"
|
"${a.subSequence(end, a.length)}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +313,11 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (media.anime.op.isNotEmpty()) {
|
if (media.anime.op.isNotEmpty()) {
|
||||||
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false)
|
val bind = ItemTitleTextBinding.inflate(
|
||||||
|
LayoutInflater.from(context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
bind.itemTitle.setText(R.string.opening)
|
bind.itemTitle.setText(R.string.opening)
|
||||||
makeText(bind.itemText, media.anime.op)
|
makeText(bind.itemText, media.anime.op)
|
||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
@@ -278,14 +325,18 @@ class MediaInfoFragment : Fragment() {
|
|||||||
|
|
||||||
|
|
||||||
if (media.anime.ed.isNotEmpty()) {
|
if (media.anime.ed.isNotEmpty()) {
|
||||||
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false)
|
val bind = ItemTitleTextBinding.inflate(
|
||||||
|
LayoutInflater.from(context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
bind.itemTitle.setText(R.string.ending)
|
bind.itemTitle.setText(R.string.ending)
|
||||||
makeText(bind.itemText, media.anime.ed)
|
makeText(bind.itemText, media.anime.ed)
|
||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.genres.isNotEmpty()) {
|
if (media.genres.isNotEmpty() && !offline) {
|
||||||
val bind = ActivityGenreBinding.inflate(
|
val bind = ActivityGenreBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
@@ -316,7 +367,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.tags.isNotEmpty()) {
|
if (media.tags.isNotEmpty() && !offline) {
|
||||||
val bind = ItemTitleChipgroupBinding.inflate(
|
val bind = ItemTitleChipgroupBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
@@ -357,7 +408,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!media.characters.isNullOrEmpty()) {
|
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||||
val bind = ItemTitleRecyclerBinding.inflate(
|
val bind = ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
@@ -374,7 +425,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!media.relations.isNullOrEmpty()) {
|
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||||
if (media.sequel != null || media.prequel != null) {
|
if (media.sequel != null || media.prequel != null) {
|
||||||
val bind = ItemQuelsBinding.inflate(
|
val bind = ItemQuelsBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
@@ -437,7 +488,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bindi.root)
|
parent.addView(bindi.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!media.recommendations.isNullOrEmpty()) {
|
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
||||||
val bind = ItemTitleRecyclerBinding.inflate(
|
val bind = ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
@@ -458,7 +509,8 @@ class MediaInfoFragment : Fragment() {
|
|||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200)
|
val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200)
|
||||||
val cornerNotTop = ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
|
val cornerNotTop =
|
||||||
|
ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
|
||||||
var cornered = true
|
var cornered = true
|
||||||
cornerTop.start()
|
cornerTop.start()
|
||||||
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->
|
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||||
|
import com.google.android.material.materialswitch.MaterialSwitch
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -27,7 +27,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
private var _binding: BottomSheetMediaListBinding? = null
|
private var _binding: BottomSheetMediaListBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetMediaListBinding.inflate(inflater, container, false)
|
_binding = BottomSheetMediaListBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -46,8 +50,12 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
binding.mediaListLayout.visibility = View.VISIBLE
|
binding.mediaListLayout.visibility = View.VISIBLE
|
||||||
|
|
||||||
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
||||||
val statusStrings = if (media?.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
|
val statusStrings =
|
||||||
val userStatus = if(media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
|
if (media?.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
|
||||||
|
R.array.status_manga
|
||||||
|
)
|
||||||
|
val userStatus =
|
||||||
|
if (media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
|
||||||
|
|
||||||
binding.mediaListStatus.setText(userStatus)
|
binding.mediaListStatus.setText(userStatus)
|
||||||
binding.mediaListStatus.setAdapter(
|
binding.mediaListStatus.setAdapter(
|
||||||
@@ -160,7 +168,9 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
val init =
|
val init =
|
||||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||||
.toInt() else 0
|
.toInt() else 0
|
||||||
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
|
if (init < (total
|
||||||
|
?: 5000)
|
||||||
|
) binding.mediaListProgress.setText((init + 1).toString())
|
||||||
if (init + 1 == (total ?: 5000)) {
|
if (init + 1 == (total ?: 5000)) {
|
||||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||||
onComplete()
|
onComplete()
|
||||||
@@ -186,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
media?.inCustomListsOf?.forEach {
|
media?.inCustomListsOf?.forEach {
|
||||||
SwitchMaterial(requireContext()).apply {
|
MaterialSwitch(requireContext()).apply {
|
||||||
isChecked = it.value
|
isChecked = it.value
|
||||||
text = it.key
|
text = it.key
|
||||||
setOnCheckedChangeListener { _, isChecked ->
|
setOnCheckedChangeListener { _, isChecked ->
|
||||||
@@ -201,11 +211,15 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
val progress =
|
||||||
|
_binding?.mediaListProgress?.text.toString().toIntOrNull()
|
||||||
val score =
|
val score =
|
||||||
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
|
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()
|
||||||
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
?.times(10))?.toInt()
|
||||||
val rewatch = _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
|
val status =
|
||||||
|
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||||
|
val rewatch =
|
||||||
|
_binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
|
||||||
val notes = _binding?.mediaListNotes?.text?.toString()
|
val notes = _binding?.mediaListNotes?.text?.toString()
|
||||||
val startD = start.date
|
val startD = start.date
|
||||||
val endD = end.date
|
val endD = end.date
|
||||||
@@ -245,7 +259,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Anilist.mutation.deleteList(id)
|
Anilist.mutation.deleteList(id)
|
||||||
MAL.query.deleteList(media?.anime!=null,media?.idMAL)
|
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||||
}
|
}
|
||||||
Refresh.all()
|
Refresh.all()
|
||||||
snackString(getString(R.string.deleted_from_list))
|
snackString(getString(R.string.deleted_from_list))
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ import androidx.core.view.updateLayoutParams
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
|
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.themes.ThemeManager
|
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -46,7 +44,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
private var _binding: BottomSheetMediaListSmallBinding? = null
|
private var _binding: BottomSheetMediaListSmallBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false)
|
_binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -60,8 +62,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
binding.mediaListProgressBar.visibility = View.GONE
|
binding.mediaListProgressBar.visibility = View.GONE
|
||||||
binding.mediaListLayout.visibility = View.VISIBLE
|
binding.mediaListLayout.visibility = View.VISIBLE
|
||||||
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
||||||
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
|
val statusStrings =
|
||||||
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
|
||||||
|
R.array.status_manga
|
||||||
|
)
|
||||||
|
val userStatus =
|
||||||
|
if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
||||||
|
|
||||||
binding.mediaListStatus.setText(userStatus)
|
binding.mediaListStatus.setText(userStatus)
|
||||||
binding.mediaListStatus.setAdapter(
|
binding.mediaListStatus.setAdapter(
|
||||||
@@ -130,10 +136,26 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
||||||
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
|
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()
|
||||||
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
?.times(10))?.toInt()
|
||||||
Anilist.mutation.editList(media.id, progress, score, null, null, status, media.isListPrivate)
|
val status =
|
||||||
MAL.query.editList(media.idMAL, media.anime != null, progress, score, status)
|
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||||
|
Anilist.mutation.editList(
|
||||||
|
media.id,
|
||||||
|
progress,
|
||||||
|
score,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
status,
|
||||||
|
media.isListPrivate
|
||||||
|
)
|
||||||
|
MAL.query.editList(
|
||||||
|
media.idMAL,
|
||||||
|
media.anime != null,
|
||||||
|
progress,
|
||||||
|
score,
|
||||||
|
status
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Refresh.all()
|
Refresh.all()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
class OtherDetailsViewModel : ViewModel() {
|
class OtherDetailsViewModel : ViewModel() {
|
||||||
private val character: MutableLiveData<Character> = MutableLiveData(null)
|
private val character: MutableLiveData<Character> = MutableLiveData(null)
|
||||||
@@ -19,26 +19,28 @@ class OtherDetailsViewModel : ViewModel() {
|
|||||||
suspend fun loadStudio(m: Studio) {
|
suspend fun loadStudio(m: Studio) {
|
||||||
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
|
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val author: MutableLiveData<Author> = MutableLiveData(null)
|
private val author: MutableLiveData<Author> = MutableLiveData(null)
|
||||||
fun getAuthor(): LiveData<Author> = author
|
fun getAuthor(): LiveData<Author> = author
|
||||||
suspend fun loadAuthor(m: Author) {
|
suspend fun loadAuthor(m: Author) {
|
||||||
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
|
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
|
||||||
}
|
}
|
||||||
private val calendar: MutableLiveData<Map<String,MutableList<Media>>> = MutableLiveData(null)
|
|
||||||
fun getCalendar(): LiveData<Map<String,MutableList<Media>>> = calendar
|
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
|
||||||
|
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
||||||
suspend fun loadCalendar() {
|
suspend fun loadCalendar() {
|
||||||
val curr = System.currentTimeMillis()/1000
|
val curr = System.currentTimeMillis() / 1000
|
||||||
val res = Anilist.query.recentlyUpdated(false,curr-86400,curr+(86400*6))
|
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
|
||||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||||
val map = mutableMapOf<String,MutableList<Media>>()
|
val map = mutableMapOf<String, MutableList<Media>>()
|
||||||
val idMap = mutableMapOf<String,MutableList<Int>>()
|
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||||
res?.forEach {
|
res?.forEach {
|
||||||
val v = it.relation?.split(",")?.map { i-> i.toLong() }!!
|
val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
|
||||||
val dateInfo = df.format(Date(v[1]*1000))
|
val dateInfo = df.format(Date(v[1] * 1000))
|
||||||
val list = map.getOrPut(dateInfo) { mutableListOf() }
|
val list = map.getOrPut(dateInfo) { mutableListOf() }
|
||||||
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
|
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
|
||||||
it.relation = "Episode ${v[0]}"
|
it.relation = "Episode ${v[0]}"
|
||||||
if(!idList.contains(it.id)) {
|
if (!idList.contains(it.id)) {
|
||||||
idList.add(it.id)
|
idList.add(it.id)
|
||||||
list.add(it)
|
list.add(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
|||||||
var bar: ProgressBar? = null
|
var bar: ProgressBar? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
|
||||||
val binding = ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return ProgressViewHolder(binding)
|
return ProgressViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +34,12 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
|||||||
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
|
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
|
||||||
override fun onDoubleClick(event: MotionEvent) {
|
override fun onDoubleClick(event: MotionEvent) {
|
||||||
snackString(currContext()?.getString(R.string.cant_wait))
|
snackString(currContext()?.getString(R.string.cant_wait))
|
||||||
ObjectAnimator.ofFloat(progressBar, "translationX", progressBar.translationX, progressBar.translationX + 100f)
|
ObjectAnimator.ofFloat(
|
||||||
|
progressBar,
|
||||||
|
"translationX",
|
||||||
|
progressBar.translationX,
|
||||||
|
progressBar.translationX + 100f
|
||||||
|
)
|
||||||
.setDuration(300).start()
|
.setDuration(300).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +57,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
|
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||||
import ani.dantotsu.connections.anilist.SearchResults
|
import ani.dantotsu.connections.anilist.SearchResults
|
||||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -33,14 +34,15 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
private lateinit var mediaAdaptor: MediaAdaptor
|
private lateinit var mediaAdaptor: MediaAdaptor
|
||||||
private lateinit var progressAdapter: ProgressAdapter
|
private lateinit var progressAdapter: ProgressAdapter
|
||||||
private lateinit var concatAdapter: ConcatAdapter
|
private lateinit var concatAdapter: ConcatAdapter
|
||||||
|
private lateinit var headerAdaptor: SearchAdapter
|
||||||
|
|
||||||
lateinit var result: SearchResults
|
lateinit var result: SearchResults
|
||||||
lateinit var updateChips: (()->Unit)
|
lateinit var updateChips: (() -> Unit)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivitySearchBinding.inflate(layoutInflater)
|
binding = ActivitySearchBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
@@ -51,7 +53,7 @@ ThemeManager(this).applyTheme()
|
|||||||
bottom = navBarHeight + 80f.px
|
bottom = navBarHeight + 80f.px
|
||||||
)
|
)
|
||||||
|
|
||||||
style = loadData<Int>("searchStyle") ?: 0
|
style = PrefManager.getVal(PrefName.SearchStyle)
|
||||||
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
|
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
|
||||||
if (!listOnly!!) listOnly = null
|
if (!listOnly!!) listOnly = null
|
||||||
|
|
||||||
@@ -76,17 +78,17 @@ ThemeManager(this).applyTheme()
|
|||||||
|
|
||||||
progressAdapter = ProgressAdapter(searched = model.searched)
|
progressAdapter = ProgressAdapter(searched = model.searched)
|
||||||
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
|
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)
|
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
override fun getSpanSize(position: Int): Int {
|
override fun getSpanSize(position: Int): Int {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> gridSize
|
0 -> gridSize
|
||||||
concatAdapter.itemCount - 1 -> gridSize
|
concatAdapter.itemCount - 1 -> gridSize
|
||||||
else -> when (style) {
|
else -> when (style) {
|
||||||
0 -> 1
|
0 -> 1
|
||||||
else -> gridSize
|
else -> gridSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,17 +151,26 @@ ThemeManager(this).applyTheme()
|
|||||||
} else
|
} else
|
||||||
headerAdaptor.requestFocus?.run()
|
headerAdaptor.requestFocus?.run()
|
||||||
|
|
||||||
if(intent.getBooleanExtra("search",false)) search()
|
if (intent.getBooleanExtra("search", false)) search()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 searchTimer = Timer()
|
||||||
private var loading = false
|
private var loading = false
|
||||||
fun search() {
|
fun search() {
|
||||||
|
headerAdaptor.setHistoryVisibility(false)
|
||||||
val size = model.searchResults.results.size
|
val size = model.searchResults.results.size
|
||||||
model.searchResults.results.clear()
|
model.searchResults.results.clear()
|
||||||
runOnUiThread {
|
binding.searchRecyclerView.post {
|
||||||
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +199,7 @@ ThemeManager(this).applyTheme()
|
|||||||
|
|
||||||
var state: Parcelable? = null
|
var state: Parcelable? = null
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
headerAdaptor.addHistory()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
|
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,71 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AlphaAnimation
|
||||||
|
import android.view.animation.Animation
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||||
|
import ani.dantotsu.App.Companion.context
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
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 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) : RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
class SearchAdapter(private val activity: SearchActivity, private val type: String) :
|
||||||
|
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
||||||
private val itemViewType = 6969
|
private val itemViewType = 6969
|
||||||
var search: Runnable? = null
|
var search: Runnable? = null
|
||||||
var requestFocus: Runnable? = null
|
var requestFocus: Runnable? = null
|
||||||
private var textWatcher: TextWatcher? = null
|
private var textWatcher: TextWatcher? = null
|
||||||
|
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
||||||
|
private lateinit var binding: ItemSearchHeaderBinding
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||||
val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return SearchHeaderViewHolder(binding)
|
return SearchHeaderViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
|
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
|
val imm: InputMethodManager =
|
||||||
|
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
|
||||||
when (activity.style) {
|
when (activity.style) {
|
||||||
0 -> {
|
0 -> {
|
||||||
binding.searchResultGrid.alpha = 1f
|
binding.searchResultGrid.alpha = 1f
|
||||||
binding.searchResultList.alpha = 0.33f
|
binding.searchResultList.alpha = 0.33f
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
binding.searchResultList.alpha = 1f
|
binding.searchResultList.alpha = 1f
|
||||||
binding.searchResultGrid.alpha = 0.33f
|
binding.searchResultGrid.alpha = 0.33f
|
||||||
@@ -50,6 +73,13 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.searchBar.hint = activity.result.type
|
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 adult = activity.result.isAdult
|
||||||
var listOnly = activity.result.onList
|
var listOnly = activity.result.onList
|
||||||
|
|
||||||
@@ -62,7 +92,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
||||||
activity.updateChips = { it.update() }
|
activity.updateChips = { it.update() }
|
||||||
}
|
}
|
||||||
binding.searchChipRecycler.layoutManager = LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
binding.searchChipRecycler.layoutManager =
|
||||||
|
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||||
|
|
||||||
binding.searchFilter.setOnClickListener {
|
binding.searchFilter.setOnClickListener {
|
||||||
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
||||||
@@ -70,10 +101,14 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
|
|
||||||
fun searchTitle() {
|
fun searchTitle() {
|
||||||
activity.result.apply {
|
activity.result.apply {
|
||||||
search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
|
search =
|
||||||
|
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
|
||||||
onList = listOnly
|
onList = listOnly
|
||||||
isAdult = adult
|
isAdult = adult
|
||||||
}
|
}
|
||||||
|
if (binding.searchBarText.text.toString().equals("hentai", true)) {
|
||||||
|
openLinkInBrowser("https://www.youtube.com/watch?v=GgJrEOo0QoA")
|
||||||
|
}
|
||||||
activity.search()
|
activity.search()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +118,18 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: 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)
|
binding.searchBarText.addTextChangedListener(textWatcher)
|
||||||
@@ -96,7 +142,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.searchBar.setEndIconOnClickListener { searchTitle() }
|
binding.searchBar.setEndIconOnClickListener { searchTitle() }
|
||||||
@@ -105,14 +152,14 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
it.alpha = 1f
|
it.alpha = 1f
|
||||||
binding.searchResultList.alpha = 0.33f
|
binding.searchResultList.alpha = 0.33f
|
||||||
activity.style = 0
|
activity.style = 0
|
||||||
saveData("searchStyle", 0)
|
PrefManager.setVal(PrefName.SearchStyle, 0)
|
||||||
activity.recycler()
|
activity.recycler()
|
||||||
}
|
}
|
||||||
binding.searchResultList.setOnClickListener {
|
binding.searchResultList.setOnClickListener {
|
||||||
it.alpha = 1f
|
it.alpha = 1f
|
||||||
binding.searchResultGrid.alpha = 0.33f
|
binding.searchResultGrid.alpha = 0.33f
|
||||||
activity.style = 1
|
activity.style = 1
|
||||||
saveData("searchStyle", 1)
|
PrefManager.setVal(PrefName.SearchStyle, 1)
|
||||||
activity.recycler()
|
activity.recycler()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +174,7 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
binding.searchList.apply {
|
binding.searchList.apply {
|
||||||
if (Anilist.userid != null) {
|
if (Anilist.userid != null) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
checkedState = when(listOnly){
|
checkedState = when (listOnly) {
|
||||||
null -> STATE_UNCHECKED
|
null -> STATE_UNCHECKED
|
||||||
true -> STATE_CHECKED
|
true -> STATE_CHECKED
|
||||||
false -> STATE_INDETERMINATE
|
false -> STATE_INDETERMINATE
|
||||||
@@ -135,10 +182,10 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
|
|
||||||
addOnCheckedStateChangedListener { _, state ->
|
addOnCheckedStateChangedListener { _, state ->
|
||||||
listOnly = when (state) {
|
listOnly = when (state) {
|
||||||
STATE_CHECKED -> true
|
STATE_CHECKED -> true
|
||||||
STATE_INDETERMINATE -> false
|
STATE_INDETERMINATE -> false
|
||||||
STATE_UNCHECKED -> null
|
STATE_UNCHECKED -> null
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,23 +202,61 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
|||||||
requestFocus = Runnable { binding.searchBarText.requestFocus() }
|
requestFocus = Runnable { binding.searchBarText.requestFocus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setHistoryVisibility(visible: Boolean) {
|
||||||
|
if (visible) {
|
||||||
|
binding.searchResultLayout.startAnimation(fadeOutAnimation())
|
||||||
|
binding.searchHistoryList.startAnimation(fadeInAnimation())
|
||||||
|
binding.searchResultLayout.visibility = View.GONE
|
||||||
|
binding.searchHistoryList.visibility = View.VISIBLE
|
||||||
|
} 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
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return itemViewType
|
return itemViewType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchChipAdapter(val activity: SearchActivity) : RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
class SearchChipAdapter(val activity: SearchActivity) :
|
||||||
|
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||||
private var chips = activity.result.toChipList()
|
private var chips = activity.result.toChipList()
|
||||||
|
|
||||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
||||||
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return SearchChipViewHolder(binding)
|
return SearchChipViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,19 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||||
private var _binding: BottomSheetSearchFilterBinding? = null
|
private var _binding: BottomSheetSearchFilterBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var activity: SearchActivity
|
private lateinit var activity: SearchActivity
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false)
|
_binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -99,7 +104,8 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
|||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
binding.root.context,
|
binding.root.context,
|
||||||
R.layout.item_dropdown,
|
R.layout.item_dropdown,
|
||||||
(1970 until 2024).map { it.toString() }.reversed().toTypedArray()
|
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
|
||||||
|
.reversed().toTypedArray()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -129,24 +135,25 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
binding.searchGenresGrid.isChecked = false
|
binding.searchGenresGrid.isChecked = false
|
||||||
|
|
||||||
binding.searchFilterTags.adapter = FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
binding.searchFilterTags.adapter =
|
||||||
val tag = chip.text.toString()
|
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
||||||
chip.isChecked = selectedTags.contains(tag)
|
val tag = chip.text.toString()
|
||||||
chip.isCloseIconVisible = exTags.contains(tag)
|
chip.isChecked = selectedTags.contains(tag)
|
||||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
chip.isCloseIconVisible = exTags.contains(tag)
|
||||||
if (isChecked) {
|
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||||
chip.isCloseIconVisible = false
|
if (isChecked) {
|
||||||
exTags.remove(tag)
|
chip.isCloseIconVisible = false
|
||||||
selectedTags.add(tag)
|
exTags.remove(tag)
|
||||||
} else
|
selectedTags.add(tag)
|
||||||
selectedTags.remove(tag)
|
} else
|
||||||
|
selectedTags.remove(tag)
|
||||||
|
}
|
||||||
|
chip.setOnLongClickListener {
|
||||||
|
chip.isChecked = false
|
||||||
|
chip.isCloseIconVisible = true
|
||||||
|
exTags.add(tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chip.setOnLongClickListener {
|
|
||||||
chip.isChecked = false
|
|
||||||
chip.isCloseIconVisible = true
|
|
||||||
exTags.add(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked ->
|
binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked ->
|
||||||
binding.searchFilterTags.layoutManager =
|
binding.searchFilterTags.layoutManager =
|
||||||
if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||||
@@ -158,10 +165,12 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) :
|
class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) :
|
||||||
RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() {
|
RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() {
|
||||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
|
||||||
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return SearchChipViewHolder(binding)
|
return SearchChipViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ abstract class SourceAdapter(
|
|||||||
private val scope: CoroutineScope
|
private val scope: CoroutineScope
|
||||||
) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() {
|
) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder {
|
||||||
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding =
|
||||||
|
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return SourceViewHolder(binding)
|
return SourceViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ abstract class SourceAdapter(
|
|||||||
|
|
||||||
abstract suspend fun onItemClick(source: ShowResponse)
|
abstract suspend fun onItemClick(source: ShowResponse)
|
||||||
|
|
||||||
inner class SourceViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class SourceViewHolder(val binding: ItemCharacterBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
dialogFragment.dismiss()
|
dialogFragment.dismiss()
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.BottomSheetDialogFragment
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
import ani.dantotsu.media.anime.AnimeSourceAdapter
|
|
||||||
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
|
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
|
||||||
|
import ani.dantotsu.media.anime.AnimeSourceAdapter
|
||||||
import ani.dantotsu.media.manga.MangaSourceAdapter
|
import ani.dantotsu.media.manga.MangaSourceAdapter
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
@@ -38,7 +38,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
var id: Int? = null
|
var id: Int? = null
|
||||||
var media: Media? = null
|
var media: Media? = null
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
_binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false)
|
_binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -47,7 +51,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||||
|
|
||||||
val scope = requireActivity().lifecycleScope
|
val scope = requireActivity().lifecycleScope
|
||||||
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val imm =
|
||||||
|
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
model.getMedia().observe(viewLifecycleOwner) {
|
model.getMedia().observe(viewLifecycleOwner) {
|
||||||
media = it
|
media = it
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
@@ -65,6 +70,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
anime = false
|
anime = false
|
||||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search() {
|
fun search() {
|
||||||
binding.searchBarText.clearFocus()
|
binding.searchBarText.clearFocus()
|
||||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||||
@@ -86,7 +92,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
search()
|
search()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.searchBar.setEndIconOnClickListener { search() }
|
binding.searchBar.setEndIconOnClickListener { search() }
|
||||||
@@ -101,7 +108,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
|
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
|
||||||
binding.searchRecyclerView.layoutManager = GridLayoutManager(
|
binding.searchRecyclerView.layoutManager = GridLayoutManager(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
clamp(requireActivity().resources.displayMetrics.widthPixels / 124f.px, 1, 4)
|
clamp(
|
||||||
|
requireActivity().resources.displayMetrics.widthPixels / 124f.px,
|
||||||
|
1,
|
||||||
|
4
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.EmptyAdapter
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityStudioBinding
|
import ani.dantotsu.databinding.ActivityStudioBinding
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.px
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.others.LangSet
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -30,8 +35,8 @@ class StudioActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
LangSet.setLocale(this)
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityStudioBinding.inflate(layoutInflater)
|
binding = ActivityStudioBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.parsers.SubtitleType
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
|
import ani.dantotsu.snackString
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.FileInputStream
|
|
||||||
|
|
||||||
class SubtitleDownloader {
|
class SubtitleDownloader {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//doesn't really download the subtitles -\_(o_o)_/-
|
//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) {
|
withContext(Dispatchers.IO) {
|
||||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
@@ -30,12 +29,12 @@ class SubtitleDownloader {
|
|||||||
|
|
||||||
// Check if response is successful
|
// Check if response is successful
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val responseBody = response.body?.string()
|
val responseBody = response.body.string()
|
||||||
|
|
||||||
|
|
||||||
val subtitleType = when {
|
val subtitleType = when {
|
||||||
responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS
|
responseBody.contains("[Script Info]") -> SubtitleType.ASS
|
||||||
responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT
|
responseBody.contains("WEBVTT") -> SubtitleType.VTT
|
||||||
else -> SubtitleType.SRT
|
else -> SubtitleType.SRT
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,5 +43,50 @@ class SubtitleDownloader {
|
|||||||
return@withContext SubtitleType.UNKNOWN
|
return@withContext SubtitleType.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//actually downloads lol
|
||||||
|
suspend fun downloadSubtitle(
|
||||||
|
context: Context,
|
||||||
|
url: String,
|
||||||
|
downloadedType: DownloadedType
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val directory = DownloadsManager.getDirectory(
|
||||||
|
context,
|
||||||
|
downloadedType.type,
|
||||||
|
downloadedType.title,
|
||||||
|
downloadedType.chapter
|
||||||
|
)
|
||||||
|
if (!directory.exists()) { //just in case
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
val type = loadSubtitleType(context, url)
|
||||||
|
val subtiteFile = File(directory, "subtitle.${type}")
|
||||||
|
if (subtiteFile.exists()) {
|
||||||
|
subtiteFile.delete()
|
||||||
|
}
|
||||||
|
subtiteFile.createNewFile()
|
||||||
|
|
||||||
|
val client = Injekt.get<NetworkHelper>().client
|
||||||
|
val request = Request.Builder().url(url).build()
|
||||||
|
val reponse = client.newCall(request).execute()
|
||||||
|
|
||||||
|
if (!reponse.isSuccessful) {
|
||||||
|
snackString("Failed to download subtitle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reponse.body.byteStream().use { input ->
|
||||||
|
subtiteFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snackString("Failed to download subtitle")
|
||||||
|
e.printStackTrace()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.databinding.ItemTitleBinding
|
import ani.dantotsu.databinding.ItemTitleBinding
|
||||||
|
|
||||||
class TitleAdapter(private val text: String) : RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
|
class TitleAdapter(private val text: String) :
|
||||||
inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root)
|
RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
|
||||||
|
inner class TitleViewHolder(val binding: ItemTitleBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
|
||||||
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ data class Anime(
|
|||||||
var ed: ArrayList<String> = arrayListOf(),
|
var ed: ArrayList<String> = arrayListOf(),
|
||||||
|
|
||||||
var mainStudio: Studio? = null,
|
var mainStudio: Studio? = null,
|
||||||
var author: Author?=null,
|
var author: Author? = null,
|
||||||
|
|
||||||
var youtube: String? = null,
|
var youtube: String? = null,
|
||||||
var nextAiringEpisode: Int? = null,
|
var nextAiringEpisode: Int? = null,
|
||||||
|
|||||||
127
app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt
Normal file
127
app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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 seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||||
|
|
||||||
|
return if (seasonMatcher.find()) {
|
||||||
|
seasonMatcher.group(2)?.toInt()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findEpisodeNumber(text: String): Float? {
|
||||||
|
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||||
|
|
||||||
|
return if (episodeMatcher.find()) {
|
||||||
|
if (episodeMatcher.group(2) != null) {
|
||||||
|
episodeMatcher.group(2)?.toFloat()
|
||||||
|
} else {
|
||||||
|
val failedEpisodeNumberPattern: Pattern =
|
||||||
|
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val failedEpisodeNumberMatcher: Matcher =
|
||||||
|
failedEpisodeNumberPattern.matcher(text)
|
||||||
|
if (failedEpisodeNumberMatcher.find()) {
|
||||||
|
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeEpisodeNumber(text: String): String {
|
||||||
|
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||||
|
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
val letterPattern = Regex("[a-zA-Z]")
|
||||||
|
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||||
|
removedNumber
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun removeEpisodeNumberCompletely(text: String): String {
|
||||||
|
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||||
|
val removedNumber = text.replace(regexPattern, "")
|
||||||
|
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||||
|
val failedEpisodeNumberPattern: Regex =
|
||||||
|
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
||||||
|
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||||
|
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removedNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +1,41 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.FrameLayout
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
|
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||||
|
import ani.dantotsu.others.LanguageMapper
|
||||||
|
import ani.dantotsu.others.webview.CookieCatcher
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||||
import ani.dantotsu.parsers.WatchSources
|
import ani.dantotsu.parsers.WatchSources
|
||||||
import ani.dantotsu.settings.ExtensionsActivity
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.tabs.TabLayout
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.lang.IndexOutOfBoundsException
|
|
||||||
|
|
||||||
class AnimeWatchAdapter(
|
class AnimeWatchAdapter(
|
||||||
private val media: Media,
|
private val media: Media,
|
||||||
@@ -57,13 +51,16 @@ class AnimeWatchAdapter(
|
|||||||
return ViewHolder(bind)
|
return ViewHolder(bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var nestedDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
_binding = binding
|
_binding = binding
|
||||||
|
|
||||||
//Youtube
|
//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.visibility = View.VISIBLE
|
||||||
binding.animeSourceYT.setOnClickListener {
|
binding.animeSourceYT.setOnClickListener {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
|
||||||
@@ -72,23 +69,42 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
|
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
|
||||||
binding.animeSourceDubbedText.text = if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed)
|
binding.animeSourceDubbedText.text =
|
||||||
|
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
||||||
|
R.string.subbed
|
||||||
|
)
|
||||||
|
|
||||||
//PreferDub
|
//PreferDub
|
||||||
var changing = false
|
var changing = false
|
||||||
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
|
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
|
||||||
binding.animeSourceDubbedText.text = if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed)
|
binding.animeSourceDubbedText.text =
|
||||||
|
if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
||||||
|
R.string.subbed
|
||||||
|
)
|
||||||
if (!changing) fragment.onDubClicked(isChecked)
|
if (!changing) fragment.onDubClicked(isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Wrong Title
|
//Wrong Title
|
||||||
binding.animeSourceSearch.setOnClickListener {
|
binding.animeSourceSearch.setOnClickListener {
|
||||||
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null)
|
SourceSearchDialogFragment().show(
|
||||||
|
fragment.requireActivity().supportFragmentManager,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
val offline = if (!isOnline(binding.root.context) || 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
|
//Source Selection
|
||||||
var source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
var source =
|
||||||
setLanguageList(media.selected!!.langIndex,source)
|
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||||
|
setLanguageList(media.selected!!.langIndex, source)
|
||||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||||
binding.animeSource.setText(watchSources.names[source])
|
binding.animeSource.setText(watchSources.names[source])
|
||||||
watchSources[source].apply {
|
watchSources[source].apply {
|
||||||
@@ -96,11 +112,17 @@ class AnimeWatchAdapter(
|
|||||||
binding.animeSourceTitle.text = showUserText
|
binding.animeSourceTitle.text = showUserText
|
||||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
binding.animeSourceDubbedCont.visibility =
|
binding.animeSourceDubbedCont.visibility =
|
||||||
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, watchSources.names))
|
binding.animeSource.setAdapter(
|
||||||
|
ArrayAdapter(
|
||||||
|
fragment.requireContext(),
|
||||||
|
R.layout.item_dropdown,
|
||||||
|
watchSources.names
|
||||||
|
)
|
||||||
|
)
|
||||||
binding.animeSourceTitle.isSelected = true
|
binding.animeSourceTitle.isSelected = true
|
||||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||||
fragment.onSourceChange(i).apply {
|
fragment.onSourceChange(i).apply {
|
||||||
@@ -109,9 +131,10 @@ class AnimeWatchAdapter(
|
|||||||
changing = true
|
changing = true
|
||||||
binding.animeSourceDubbed.isChecked = selectDub
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
changing = false
|
changing = false
|
||||||
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
binding.animeSourceDubbedCont.visibility =
|
||||||
|
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||||
source = i
|
source = i
|
||||||
setLanguageList(0,i)
|
setLanguageList(0, i)
|
||||||
}
|
}
|
||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
fragment.loadEpisodes(i, false)
|
fragment.loadEpisodes(i, false)
|
||||||
@@ -124,11 +147,13 @@ class AnimeWatchAdapter(
|
|||||||
fragment.onLangChange(i)
|
fragment.onLangChange(i)
|
||||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||||
binding.animeSourceTitle.text = showUserText
|
binding.animeSourceTitle.text = showUserText
|
||||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
showUserTextListener =
|
||||||
|
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
changing = true
|
changing = true
|
||||||
binding.animeSourceDubbed.isChecked = selectDub
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
changing = false
|
changing = false
|
||||||
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
binding.animeSourceDubbedCont.visibility =
|
||||||
|
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||||
setLanguageList(i, source)
|
setLanguageList(i, source)
|
||||||
}
|
}
|
||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
@@ -144,9 +169,10 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Icons
|
||||||
|
|
||||||
//Subscription
|
//subscribe
|
||||||
subscribe = MediaDetailsActivity.PopImageButton(
|
subscribe = MediaDetailsActivity.PopImageButton(
|
||||||
fragment.lifecycleScope,
|
fragment.lifecycleScope,
|
||||||
binding.animeSourceSubscribe,
|
binding.animeSourceSubscribe,
|
||||||
R.drawable.ic_round_notifications_active_24,
|
R.drawable.ic_round_notifications_active_24,
|
||||||
@@ -161,51 +187,115 @@ class AnimeWatchAdapter(
|
|||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
|
|
||||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||||
openSettings(fragment.requireContext(),getChannelId(true,media.id))
|
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
//Icons
|
//Nested Button
|
||||||
var reversed = media.selected!!.recyclerReversed
|
binding.animeNestedButton.setOnClickListener {
|
||||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
val dialogView =
|
||||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||||
binding.animeSourceTop.setOnClickListener {
|
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||||
reversed = !reversed
|
var refresh = false
|
||||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
var run = false
|
||||||
fragment.onIconPressed(style, reversed)
|
var reversed = media.selected!!.recyclerReversed
|
||||||
}
|
var style =
|
||||||
var selected = when (style) {
|
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
|
||||||
0 -> binding.animeSourceList
|
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||||
1 -> binding.animeSourceGrid
|
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||||
2 -> binding.animeSourceCompact
|
dialogBinding.animeSourceTop.setOnClickListener {
|
||||||
else -> binding.animeSourceList
|
reversed = !reversed
|
||||||
}
|
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||||
selected.alpha = 1f
|
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||||
fun selected(it: ImageView) {
|
run = true
|
||||||
selected.alpha = 0.33f
|
}
|
||||||
selected = it
|
//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
|
selected.alpha = 1f
|
||||||
}
|
fun selected(it: ImageButton) {
|
||||||
binding.animeSourceList.setOnClickListener {
|
selected.alpha = 0.33f
|
||||||
selected(it as ImageView)
|
selected = it
|
||||||
style = 0
|
selected.alpha = 1f
|
||||||
fragment.onIconPressed(style, reversed)
|
}
|
||||||
}
|
dialogBinding.animeSourceList.setOnClickListener {
|
||||||
binding.animeSourceGrid.setOnClickListener {
|
selected(it as ImageButton)
|
||||||
selected(it as ImageView)
|
style = 0
|
||||||
style = 1
|
dialogBinding.layoutText.text = "List"
|
||||||
fragment.onIconPressed(style, reversed)
|
run = true
|
||||||
}
|
}
|
||||||
binding.animeSourceCompact.setOnClickListener {
|
dialogBinding.animeSourceGrid.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageButton)
|
||||||
style = 2
|
style = 1
|
||||||
fragment.onIconPressed(style, reversed)
|
dialogBinding.layoutText.text = "Grid"
|
||||||
}
|
run = true
|
||||||
|
}
|
||||||
|
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||||
|
selected(it as ImageButton)
|
||||||
|
style = 2
|
||||||
|
dialogBinding.layoutText.text = "Compact"
|
||||||
|
run = true
|
||||||
|
}
|
||||||
|
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||||
|
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||||
|
toast("WebView not installed")
|
||||||
|
}
|
||||||
|
//start CookieCatcher activity
|
||||||
|
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||||
|
val sourceAHH = watchSources[source] as? DynamicAnimeParser
|
||||||
|
val sourceHttp =
|
||||||
|
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
|
||||||
|
val url = sourceHttp?.baseUrl
|
||||||
|
url?.let {
|
||||||
|
refresh = true
|
||||||
|
val 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()
|
||||||
|
}
|
||||||
//Episode Handling
|
//Episode Handling
|
||||||
handleEpisodes()
|
handleEpisodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribeButton(enabled : Boolean) {
|
fun subscribeButton(enabled: Boolean) {
|
||||||
subscribe?.enabled(enabled)
|
subscribe?.enabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,14 +309,26 @@ class AnimeWatchAdapter(
|
|||||||
for (position in arr.indices) {
|
for (position in arr.indices) {
|
||||||
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
||||||
val chip =
|
val chip =
|
||||||
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root
|
ItemChipBinding.inflate(
|
||||||
|
LayoutInflater.from(fragment.context),
|
||||||
|
binding.animeSourceChipGroup,
|
||||||
|
false
|
||||||
|
).root
|
||||||
chip.isCheckable = true
|
chip.isCheckable = true
|
||||||
fun selected() {
|
fun selected() {
|
||||||
chip.isChecked = true
|
chip.isChecked = true
|
||||||
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
binding.animeWatchChipScroll.smoothScrollTo(
|
||||||
|
(chip.left - screenWidth / 2) + (chip.width / 2),
|
||||||
|
0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||||
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
|
chip.setTextColor(
|
||||||
|
ContextCompat.getColorStateList(
|
||||||
|
fragment.requireContext(),
|
||||||
|
R.color.chip_text_color
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
selected()
|
selected()
|
||||||
@@ -239,7 +341,14 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (select != null)
|
if (select != null)
|
||||||
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } }
|
binding.animeWatchChipScroll.apply {
|
||||||
|
post {
|
||||||
|
scrollTo(
|
||||||
|
(select.left - screenWidth / 2) + (select.width / 2),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +364,9 @@ class AnimeWatchAdapter(
|
|||||||
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
||||||
|
|
||||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
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()
|
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||||
if (episodes.contains(continueEp)) {
|
if (episodes.contains(continueEp)) {
|
||||||
@@ -267,7 +378,10 @@ class AnimeWatchAdapter(
|
|||||||
media.id,
|
media.id,
|
||||||
continueEp
|
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)
|
val e = episodes.indexOf(continueEp)
|
||||||
if (e != -1 && e + 1 < episodes.size) {
|
if (e != -1 && e + 1 < episodes.size) {
|
||||||
continueEp = episodes[e + 1]
|
continueEp = episodes[e + 1]
|
||||||
@@ -281,15 +395,23 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val ep = media.anime.episodes!![continueEp]!!
|
val ep = media.anime.episodes!![continueEp]!!
|
||||||
binding.itemEpisodeImage.loadImage(ep.thumb ?: FileUrl[media.banner ?: media.cover], 0)
|
|
||||||
|
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||||
|
|
||||||
|
binding.itemEpisodeImage.loadImage(
|
||||||
|
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
||||||
|
)
|
||||||
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
||||||
binding.animeSourceContinueText.text =
|
binding.animeSourceContinueText.text =
|
||||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
|
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
|
||||||
binding.animeSourceContinue.setOnClickListener {
|
binding.animeSourceContinue.setOnClickListener {
|
||||||
fragment.onEpisodeClick(continueEp)
|
fragment.onEpisodeClick(continueEp)
|
||||||
}
|
}
|
||||||
if (fragment.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()
|
binding.animeSourceContinue.performClick()
|
||||||
fragment.continueEp = false
|
fragment.continueEp = false
|
||||||
}
|
}
|
||||||
@@ -297,6 +419,7 @@ class AnimeWatchAdapter(
|
|||||||
} else {
|
} else {
|
||||||
binding.animeSourceContinue.visibility = View.GONE
|
binding.animeSourceContinue.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSourceProgressBar.visibility = View.GONE
|
binding.animeSourceProgressBar.visibility = View.GONE
|
||||||
if (media.anime.episodes!!.isNotEmpty())
|
if (media.anime.episodes!!.isNotEmpty())
|
||||||
binding.animeSourceNotFound.visibility = View.GONE
|
binding.animeSourceNotFound.visibility = View.GONE
|
||||||
@@ -311,7 +434,7 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLanguageList(lang: Int, source: Int) {
|
private fun setLanguageList(lang: Int, source: Int) {
|
||||||
val binding = _binding
|
val binding = _binding
|
||||||
if (watchSources is AnimeSources) {
|
if (watchSources is AnimeSources) {
|
||||||
val parser = watchSources[source] as? DynamicAnimeParser
|
val parser = watchSources[source] as? DynamicAnimeParser
|
||||||
@@ -321,10 +444,21 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||||
}catch (e: IndexOutOfBoundsException) {
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown")
|
binding?.animeSourceLanguage?.setText(
|
||||||
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,7 +466,8 @@ class AnimeWatchAdapter(
|
|||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
//Timer
|
//Timer
|
||||||
countDown(media, binding.animeSourceContainer)
|
countDown(media, binding.animeSourceContainer)
|
||||||
|
|||||||
@@ -2,37 +2,47 @@ package ani.dantotsu.media.anime
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.math.MathUtils
|
import androidx.core.math.MathUtils
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadService
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
|
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.others.LanguageMapper
|
||||||
import ani.dantotsu.parsers.AnimeParser
|
import ani.dantotsu.parsers.AnimeParser
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.HAnimeSources
|
import ani.dantotsu.parsers.HAnimeSources
|
||||||
import ani.dantotsu.settings.ExtensionsActivity
|
|
||||||
import ani.dantotsu.settings.InstalledAnimeExtensionsFragment
|
|
||||||
import ani.dantotsu.settings.PlayerSettings
|
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
|
||||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
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
|
||||||
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
@@ -40,10 +50,7 @@ import ani.dantotsu.subcriptions.SubscriptionHelper
|
|||||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.navigationrail.NavigationRailView
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -70,14 +77,14 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
private lateinit var headerAdapter: AnimeWatchAdapter
|
private lateinit var headerAdapter: AnimeWatchAdapter
|
||||||
private lateinit var episodeAdapter: EpisodeAdapter
|
private lateinit var episodeAdapter: EpisodeAdapter
|
||||||
|
|
||||||
|
val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
var screenWidth = 0f
|
var screenWidth = 0f
|
||||||
private var progress = View.VISIBLE
|
private var progress = View.VISIBLE
|
||||||
|
|
||||||
var continueEp: Boolean = false
|
var continueEp: Boolean = false
|
||||||
var loaded = false
|
var loaded = false
|
||||||
|
|
||||||
lateinit var playerSettings: PlayerSettings
|
|
||||||
lateinit var uiSettings: UserInterfaceSettings
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -89,16 +96,27 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val intentFilter = IntentFilter().apply {
|
||||||
|
addAction(ACTION_DOWNLOAD_STARTED)
|
||||||
|
addAction(ACTION_DOWNLOAD_FINISHED)
|
||||||
|
addAction(ACTION_DOWNLOAD_FAILED)
|
||||||
|
addAction(ACTION_DOWNLOAD_PROGRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
requireContext(),
|
||||||
|
downloadStatusReceiver,
|
||||||
|
intentFilter,
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||||
|
|
||||||
var maxGridSize = (screenWidth / 100f).roundToInt()
|
var maxGridSize = (screenWidth / 100f).roundToInt()
|
||||||
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
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)
|
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
|
||||||
|
|
||||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
@@ -106,11 +124,11 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
val style = episodeAdapter.getItemViewType(position)
|
val style = episodeAdapter.getItemViewType(position)
|
||||||
|
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> maxGridSize
|
0 -> maxGridSize
|
||||||
else -> when (style) {
|
else -> when (style) {
|
||||||
0 -> maxGridSize
|
0 -> maxGridSize
|
||||||
1 -> 2
|
1 -> 2
|
||||||
2 -> 1
|
2 -> 1
|
||||||
else -> maxGridSize
|
else -> maxGridSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +137,23 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
|
|
||||||
binding.animeSourceRecycler.layoutManager = gridLayoutManager
|
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) {
|
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||||
if (it) binding.animeSourceRecycler.scrollToPosition(0)
|
if (it) binding.animeSourceRecycler.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
@@ -129,7 +164,8 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
media = it
|
media = it
|
||||||
media.selected = model.loadSelected(media)
|
media.selected = model.loadSelected(media)
|
||||||
|
|
||||||
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
|
subscribed =
|
||||||
|
SubscriptionHelper.getSubscriptions().containsKey(media.id)
|
||||||
|
|
||||||
style = media.selected!!.recyclerStyle
|
style = media.selected!!.recyclerStyle
|
||||||
reverse = media.selected!!.recyclerReversed
|
reverse = media.selected!!.recyclerReversed
|
||||||
@@ -140,10 +176,20 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
||||||
|
|
||||||
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
|
val offlineMode =
|
||||||
episodeAdapter = EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
|
model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
|
||||||
|
|
||||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, episodeAdapter)
|
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
|
||||||
|
episodeAdapter =
|
||||||
|
EpisodeAdapter(
|
||||||
|
style ?: PrefManager.getVal(PrefName.AnimeDefaultView),
|
||||||
|
media,
|
||||||
|
this,
|
||||||
|
offlineMode = offlineMode
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.animeSourceRecycler.adapter =
|
||||||
|
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
awaitAll(
|
awaitAll(
|
||||||
@@ -165,15 +211,23 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
episodes.forEach { (i, episode) ->
|
episodes.forEach { (i, episode) ->
|
||||||
if (media.anime?.fillerEpisodes != null) {
|
if (media.anime?.fillerEpisodes != null) {
|
||||||
if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
|
if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
|
||||||
episode.title = episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
|
episode.title =
|
||||||
|
episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
|
||||||
episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false
|
episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (media.anime?.kitsuEpisodes != null) {
|
if (media.anime?.kitsuEpisodes != null) {
|
||||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||||
episode.desc = episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
|
episode.desc =
|
||||||
episode.title = episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
|
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||||
episode.thumb = episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ?: FileUrl[media.cover]
|
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
|
||||||
|
episode.title ?: ""
|
||||||
|
).isBlank()
|
||||||
|
) media.anime!!.kitsuEpisodes!![i]?.title
|
||||||
|
?: episode.title else episode.title
|
||||||
|
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
|
||||||
|
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
|
||||||
|
?: FileUrl[media.cover]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +241,7 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
val limit = when {
|
val limit = when {
|
||||||
(divisions < 25) -> 25
|
(divisions < 25) -> 25
|
||||||
(divisions < 50) -> 50
|
(divisions < 50) -> 50
|
||||||
else -> 100
|
else -> 100
|
||||||
}
|
}
|
||||||
headerAdapter.clearChips()
|
headerAdapter.clearChips()
|
||||||
if (total > limit) {
|
if (total > limit) {
|
||||||
@@ -229,7 +283,7 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null
|
model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null
|
||||||
selected.sourceIndex = i
|
selected.sourceIndex = i
|
||||||
selected.server = null
|
selected.server = null
|
||||||
model.saveSelected(media.id, selected, requireActivity())
|
model.saveSelected(media.id, selected)
|
||||||
media.selected = selected
|
media.selected = selected
|
||||||
return model.watchSources?.get(i)!!
|
return model.watchSources?.get(i)!!
|
||||||
}
|
}
|
||||||
@@ -237,7 +291,7 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
fun onLangChange(i: Int) {
|
fun onLangChange(i: Int) {
|
||||||
val selected = model.loadSelected(media)
|
val selected = model.loadSelected(media)
|
||||||
selected.langIndex = i
|
selected.langIndex = i
|
||||||
model.saveSelected(media.id, selected, requireActivity())
|
model.saveSelected(media.id, selected)
|
||||||
media.selected = selected
|
media.selected = selected
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,9 +299,14 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
val selected = model.loadSelected(media)
|
val selected = model.loadSelected(media)
|
||||||
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
||||||
selected.preferDub = checked
|
selected.preferDub = checked
|
||||||
model.saveSelected(media.id, selected, requireActivity())
|
model.saveSelected(media.id, selected)
|
||||||
media.selected = selected
|
media.selected = selected
|
||||||
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
model.forceLoadEpisode(
|
||||||
|
media,
|
||||||
|
selected.sourceIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadEpisodes(i: Int, invalidate: Boolean) {
|
fun loadEpisodes(i: Int, invalidate: Boolean) {
|
||||||
@@ -259,7 +318,7 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
reverse = rev
|
reverse = rev
|
||||||
media.selected!!.recyclerStyle = style
|
media.selected!!.recyclerStyle = style
|
||||||
media.selected!!.recyclerReversed = reverse
|
media.selected!!.recyclerReversed = reverse
|
||||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
model.saveSelected(media.id, media.selected!!)
|
||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +326,7 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
media.selected!!.chip = i
|
media.selected!!.chip = i
|
||||||
start = s
|
start = s
|
||||||
end = e
|
end = e
|
||||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
model.saveSelected(media.id, media.selected!!)
|
||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,66 +348,74 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
else getString(R.string.unsubscribed_notification)
|
else getString(R.string.unsubscribed_notification)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fun openSettings(pkg: AnimeExtension.Installed){
|
|
||||||
|
fun openSettings(pkg: AnimeExtension.Installed) {
|
||||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||||
val activity = requireActivity() as MediaDetailsActivity
|
val activity = activity
|
||||||
val visibility = if (show) View.VISIBLE else View.GONE
|
if (activity is MediaDetailsActivity && isAdded) {
|
||||||
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
val visibility = if (show) View.VISIBLE else View.GONE
|
||||||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||||
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||||
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||||
try{
|
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||||
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
try {
|
||||||
}catch (e: ClassCastException){
|
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||||
activity.findViewById<NavigationRailView>(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>()
|
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||||
if (allSettings.isNotEmpty()) {
|
if (allSettings.isNotEmpty()) {
|
||||||
var selectedSetting = allSettings[0]
|
var selectedSetting = allSettings[0]
|
||||||
if (allSettings.size > 1) {
|
if (allSettings.size > 1) {
|
||||||
val names = allSettings.map { it.lang }.toTypedArray()
|
val names =
|
||||||
var selectedIndex = 0
|
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||||
AlertDialog.Builder(requireContext())
|
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||||
.setTitle("Select a Source")
|
.setTitle("Select a Source")
|
||||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
.setSingleChoiceItems(names, -1) { dialog, which ->
|
||||||
selectedIndex = which
|
selectedSetting = allSettings[which]
|
||||||
}
|
itemSelected = true
|
||||||
.setPositiveButton("OK") { dialog, _ ->
|
|
||||||
selectedSetting = allSettings[selectedIndex]
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
|
||||||
// Move the fragment transaction here
|
// Move the fragment transaction here
|
||||||
val fragment =
|
requireActivity().runOnUiThread {
|
||||||
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
|
val fragment =
|
||||||
changeUIVisibility(true)
|
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||||
loadEpisodes(media.selected!!.sourceIndex, true)
|
changeUIVisibility(true)
|
||||||
}
|
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||||
parentFragmentManager.beginTransaction()
|
}
|
||||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
parentFragmentManager.beginTransaction()
|
||||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||||
.addToBackStack(null)
|
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||||
.commit()
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton("Cancel") { dialog, _ ->
|
.setOnDismissListener {
|
||||||
dialog.cancel()
|
if (!itemSelected) {
|
||||||
changeUIVisibility(true)
|
changeUIVisibility(true)
|
||||||
return@setNegativeButton
|
}
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
dialog.window?.setDimAmount(0.8f)
|
||||||
} else {
|
} else {
|
||||||
// If there's only one setting, proceed with the fragment transaction
|
// If there's only one setting, proceed with the fragment transaction
|
||||||
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
|
requireActivity().runOnUiThread {
|
||||||
changeUIVisibility(true)
|
val fragment =
|
||||||
loadEpisodes(media.selected!!.sourceIndex, true)
|
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||||
|
changeUIVisibility(true)
|
||||||
|
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||||
|
}
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||||
|
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
}
|
}
|
||||||
parentFragmentManager.beginTransaction()
|
|
||||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
|
||||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeUIVisibility(false)
|
changeUIVisibility(false)
|
||||||
@@ -358,46 +425,138 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEpisodeClick(i: String) {
|
fun onEpisodeClick(i: String) {
|
||||||
model.continueMedia = false
|
model.continueMedia = false
|
||||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
model.saveSelected(media.id, media.selected!!)
|
||||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
|
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||||
|
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
||||||
|
val cancelIntent = Intent().apply {
|
||||||
|
action = AnimeDownloaderService.ACTION_CANCEL_DOWNLOAD
|
||||||
|
putExtra(
|
||||||
|
AnimeDownloaderService.EXTRA_TASK_NAME,
|
||||||
|
AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
requireContext().sendBroadcast(cancelIntent)
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
// Remove the download from the manager and update the UI
|
||||||
private fun reload() {
|
downloadManager.removeDownload(
|
||||||
val selected = model.loadSelected(media)
|
DownloadedType(
|
||||||
|
media.mainName(),
|
||||||
|
i,
|
||||||
|
DownloadedType.Type.ANIME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
episodeAdapter.purgeDownload(i)
|
||||||
|
}
|
||||||
|
|
||||||
//Find latest episode for subscription
|
@OptIn(UnstableApi::class)
|
||||||
selected.latest =
|
fun onAnimeEpisodeRemoveDownloadClick(i: String) {
|
||||||
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
downloadManager.removeDownload(
|
||||||
selected.latest =
|
DownloadedType(
|
||||||
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
model.saveSelected(media.id, selected, requireActivity())
|
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||||
headerAdapter.handleEpisodes()
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
|
if (!this@AnimeWatchFragment::episodeAdapter.isInitialized) return
|
||||||
var arr: ArrayList<Episode> = arrayListOf()
|
when (intent.action) {
|
||||||
if (media.anime!!.episodes != null) {
|
ACTION_DOWNLOAD_STARTED -> {
|
||||||
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
|
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||||
arr.addAll(
|
chapterNumber?.let { episodeAdapter.startDownload(it) }
|
||||||
media.anime!!.episodes!!.values.toList()
|
}
|
||||||
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
|
|
||||||
)
|
ACTION_DOWNLOAD_FINISHED -> {
|
||||||
if (reverse)
|
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
|
||||||
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
episodeAdapter.arr = arr
|
|
||||||
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
|
|
||||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
model.watchSources?.flushText()
|
private fun reload() {
|
||||||
super.onDestroy()
|
val selected = model.loadSelected(media)
|
||||||
|
|
||||||
|
//Find latest episode for subscription
|
||||||
|
selected.latest =
|
||||||
|
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
||||||
|
selected.latest =
|
||||||
|
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||||
|
|
||||||
|
model.saveSelected(media.id, selected)
|
||||||
|
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) {
|
||||||
|
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
|
||||||
|
arr.addAll(
|
||||||
|
media.anime!!.episodes!!.values.toList()
|
||||||
|
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
|
||||||
|
)
|
||||||
|
if (reverse)
|
||||||
|
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
|
||||||
}
|
}
|
||||||
|
episodeAdapter.arr = arr
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var state: Parcelable? = null
|
override fun onDestroy() {
|
||||||
|
model.watchSources?.flushText()
|
||||||
|
super.onDestroy()
|
||||||
|
try {
|
||||||
|
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var state: Parcelable? = null
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
binding.mediaInfoProgressBar.visibility = progress
|
binding.mediaInfoProgressBar.visibility = progress
|
||||||
@@ -409,4 +568,12 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
|
||||||
|
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
|
||||||
|
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
|
||||||
|
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
|
||||||
|
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user