mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-12 22:27:44 +00:00
Compare commits
463 Commits
| 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 | ||
|
|
8d9254140d | ||
|
|
533aa9f56e | ||
|
|
c310bea0e9 | ||
|
|
813f7a0992 | ||
|
|
4c0f56d3e3 | ||
|
|
1f44d32f35 | ||
|
|
d937f447ef | ||
|
|
187262a266 | ||
|
|
f40ebc9d09 | ||
|
|
3998d88297 | ||
|
|
4db301ca7a | ||
|
|
3dfcc9fc31 | ||
|
|
df63586c02 | ||
|
|
d7372d4dbb | ||
|
|
f4266d0da3 | ||
|
|
736b06bdbe | ||
|
|
5543d29317 | ||
|
|
2fc351f57a | ||
|
|
eee1242964 | ||
|
|
a58e9a523a | ||
|
|
cd3aad1c33 | ||
|
|
5a482d8307 | ||
|
|
91d869005c | ||
|
|
05e73269d3 | ||
|
|
3dac48ced8 | ||
|
|
8c5726ab8a | ||
|
|
076516be23 | ||
|
|
1059a3c17e | ||
|
|
c75df942f2 | ||
|
|
cfe7be5cdb | ||
|
|
390fc18c4c | ||
|
|
4c82c56828 | ||
|
|
da5c480ba7 | ||
|
|
20acd71b1a | ||
|
|
231c9c5b98 | ||
|
|
4cfdcdb23c | ||
|
|
acb0225699 | ||
|
|
ebffaaa742 | ||
|
|
1760064555 | ||
|
|
44a6db3fc2 | ||
|
|
aab25d157e | ||
|
|
8a4be86ddc | ||
|
|
31baf729be | ||
|
|
b98e3dc780 | ||
|
|
878d58679e | ||
|
|
f500ba6cf0 | ||
|
|
d124736556 | ||
|
|
1a825e2509 | ||
|
|
6e14c2221d | ||
|
|
5b6e351a56 | ||
|
|
c310708401 | ||
|
|
f0093b903a | ||
|
|
d33568f0ad | ||
|
|
26f9f40042 | ||
|
|
7545870f38 | ||
|
|
3368a1bc8d | ||
|
|
9c0ef7a788 | ||
|
|
960c2b4113 |
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 }}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -23,8 +23,8 @@ output.json
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
#other
|
||||
scripts/
|
||||
|
||||
110
README.md
110
README.md
@@ -1,109 +1,41 @@
|
||||
# **Dantotsu** (🚧 ALPHA 🚧)
|
||||
|
||||
> ⚠️ **WARNING**: This project is in alpha stage. Things may not work as expected.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
||||
<img src="https://pbxt.replicate.delivery/2PX94viD6lJSDVayQrGyDH7CGu7IjQ6e8HEtOGDeelefXRdOC/out.png" alt="Dantotsu Banner" width=100% >
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
|
||||
<a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
|
||||
<a href="https://www.codefactor.io/repository/github/rebelonion/dantotsu"><img src="https://www.codefactor.io/repository/github/rebelonion/dantotsu/badge?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge" alt="CodeFactor" /></a>
|
||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/discord/358599430502481920.svg?style=for-the-badge&logo=discord&colorB=7289DA"></a>
|
||||
</p>
|
||||
Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-of-the-art elegance. It is an <a href="https://anilist.co/">Anilist</a> only client, which also lets you stream-download Anime & Manga through extensions.
|
||||
<br><br>
|
||||
<i>Dantotsu (断トツ; Dan-totsu) literally means the best of the best in Japanese. Well, we would like to say this is the best open source app for anime and manga on Android, but hey, try it out yourself & judge!
|
||||
</i>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||
<br>
|
||||
|
||||
### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||
# **Dantotsu** 🌟
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> Please do not attempt to upload Dantotsu or any of it's forks on Playstore or any other Android appstores on the internet. Doing so, may infringe their terms and conditions. This may result to legal action or immediate take-down of the app.
|
||||
Dantotsu is an [Anilist](https://anilist.co/) only client.
|
||||
|
||||
## Extension Status
|
||||
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
|
||||
|
||||
| Type | Status |
|
||||
| ---------------- | ------- |
|
||||
| Anime Extensions | Working |
|
||||
| Manga Extensions | "Working" |
|
||||
| Light Novel Extensions | Not Working |
|
||||
<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!
|
||||
|
||||
## WANT TO CONTRIBUTE? 🤝
|
||||
|
||||
## APP FEATURES
|
||||
All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content - whatever you have to offer, we can use!
|
||||
|
||||
- Easy and functional way to both, watch anime and read manga, ad-free.
|
||||
You can come hang out with our awesome community, request new features, and report any bugs or issues at our Discord server too. 📣
|
||||
|
||||
- A completely open source app with a nice UI & Animations :)
|
||||
### OFFICIAL DISCORD SERVER 🚀
|
||||
|
||||
- Aniyomi extension support built right into the app.
|
||||
|
||||
- Synchronize anime and manga real-time with AniList and MyAnimeList. Easily categorise anime and manga based on your current status. (Powered by AniList)
|
||||
|
||||
- Find all shows using thoroughly and frequently updated list of all trending, popular and ongoing anime based on scores.
|
||||
|
||||
- View extensive details about anime shows, movies and manga titles. It also features ability to countdown to the next episode of airing anime. (Powered by AniList & MyAnimeList)
|
||||
|
||||
- Get notified when new episodes/chapters are released!
|
||||
|
||||
|
||||
* **Available Anime sources:-**
|
||||
NONE BUILT IN!
|
||||
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
|
||||
|
||||
|
||||
* **Available Manga sources:-**
|
||||
NONE BUILT IN!
|
||||
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
|
||||
|
||||
## Planned Stuff
|
||||
|
||||
- get app out of alpha
|
||||
|
||||
- Accent Color Change (RIP Hot Pink Supremacy.)
|
||||
|
||||
|
||||
## Rejected Stuff (still rejected)
|
||||
|
||||
- Sources of any language except English
|
||||
|
||||
- News Section in the App
|
||||
|
||||
- Comment Section
|
||||
|
||||
|
||||
## WANT TO CONTRIBUTE?
|
||||
|
||||
- All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content- whatever you have to offer, we can use it!
|
||||
|
||||
- You can come hang out with our awesome community and request new features and report any bugs or issue at our discord server too.
|
||||
|
||||
### Official Discord Server
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
||||
<a href="https://discord.gg/4HPZ5nAWwM">
|
||||
<img src="https://invidget.switchblade.xyz/4HPZ5nAWwM">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## VISITORS
|
||||
|
||||
### VISIT FOR MORE INFORMATION:-
|
||||
<img src="https://count.getloli.com/get/@:rebeloniondantotsu" alt=":rebeloniondantotsu" />
|
||||
|
||||
no website yet :(
|
||||
|
||||
## DISCLAIMER
|
||||
|
||||
* Dantotsu by itself only provides an anime and manga tracker and does not provide any anime or manga streaming or downloading capabilities.
|
||||
|
||||
* Dantotsu or any of its developer/staff don't host any of the content found inside Dantotsu. Any and all images and anime/manga information found in the app are taken from various public APIs (AniList, MyAnimeList, Kitsu).
|
||||
|
||||
* Furthermore, all of the anime/manga links found in Dantotsu are taken from various 3rd party plugins and have no affiliation with Dantotsu or its staff.
|
||||
|
||||
* Dantotsu or it's owners aren't liable for any misuse of any of the contents found inside or outside of the app and cannot be held accountable for the distribution of any of the contents found inside the app.
|
||||
|
||||
* By using Dantotsu, you comply to the fact that the developer of the app is not responsible for any of the contents found in the app. You also agree to the fact that you may not use Dantotsu to download or stream any copyrighted content.
|
||||
|
||||
* If the internet infringement issues are involved, please contact the source website. The developer does not assume any legal responsibility.
|
||||
|
||||
## License
|
||||
## LICENSE 📜
|
||||
|
||||
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
||||
|
||||
4
app/.gitignore
vendored
4
app/.gitignore
vendored
@@ -1,4 +1,8 @@
|
||||
/build
|
||||
/debug
|
||||
/debug/output-metadata.json
|
||||
/alpha
|
||||
/alpha/output-metadata.json
|
||||
/google/*
|
||||
/fdroid/*
|
||||
/release
|
||||
@@ -1,12 +1,9 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
id 'kotlin-android'
|
||||
id 'kotlinx-serialization'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'com.google.devtools.ksp'
|
||||
|
||||
}
|
||||
|
||||
def gitCommitHash = providers.exec {
|
||||
@@ -20,24 +17,51 @@ android {
|
||||
applicationId "ani.dantotsu"
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "0.1.5"
|
||||
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "2.2.0"
|
||||
versionCode 220000000
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
flavorDimensions "store"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
// F-Droid specific configuration
|
||||
dimension "store"
|
||||
versionNameSuffix "-fdroid"
|
||||
}
|
||||
google {
|
||||
// Google Play specific configuration
|
||||
dimension "store"
|
||||
isDefault true
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
alpha {
|
||||
applicationIdSuffix ".beta" // keep as beta by popular request
|
||||
versionNameSuffix "-alpha01"
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"]
|
||||
debuggable System.getenv("CI") == null
|
||||
isDefault true
|
||||
}
|
||||
debug {
|
||||
//applicationIdSuffix ".beta"
|
||||
debuggable true
|
||||
versionNameSuffix "." + gitCommitHash
|
||||
applicationIdSuffix ".beta"
|
||||
versionNameSuffix "-beta01"
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
|
||||
debuggable false
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
|
||||
debuggable false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -51,20 +75,27 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// FireBase
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.1'
|
||||
// Core
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.browser:browser:1.6.0'
|
||||
implementation 'androidx.browser:browser:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.10.0'
|
||||
|
||||
// Glide
|
||||
ext.glide_version = '4.16.0'
|
||||
@@ -74,22 +105,20 @@ dependencies {
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// FireBase
|
||||
implementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx:21.3.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.4.3'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.1.1'
|
||||
ext.exo_version = '1.2.1'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||
implementation "androidx.media3:media3-session:$exo_version"
|
||||
//media3 casting
|
||||
implementation "androidx.media3:media3-cast:$exo_version"
|
||||
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
||||
|
||||
// UI
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||
@@ -97,6 +126,7 @@ dependencies {
|
||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||
|
||||
// string matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
@@ -108,16 +138,18 @@ dependencies {
|
||||
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
|
||||
implementation 'com.squareup.logcat:logcat:0.1'
|
||||
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||
implementation 'com.squareup.okio:okio:3.3.0'
|
||||
implementation 'ch.acra:acra-http:5.9.7'
|
||||
implementation 'com.squareup.okio:okio:3.7.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
|
||||
implementation 'ch.acra:acra-http:5.11.3'
|
||||
implementation 'org.jsoup:jsoup:1.15.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.5.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'app.cash.quickjs:quickjs-android:0.9.2'
|
||||
|
||||
}
|
||||
|
||||
86
app/google-services.json
Normal file
86
app/google-services.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "1039200814590",
|
||||
"project_id": "dantotsu-1e50f",
|
||||
"storage_bucket": "dantotsu-1e50f.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1039200814590:android:c372b8c1b92b825f1aacaf",
|
||||
"android_client_info": {
|
||||
"package_name": "ani.Dantotsu"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
|
||||
"android_client_info": {
|
||||
"package_name": "ani.dantotsu.beta"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
|
||||
"android_client_info": {
|
||||
"package_name": "ani.dantotsu.alpha"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
|
||||
"android_client_info": {
|
||||
"package_name": "ani.dantotsu"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
376
app/src/alpha/res/drawable/anim_splash.xml
Normal file
376
app/src/alpha/res/drawable/anim_splash.xml
Normal file
@@ -0,0 +1,376 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="768dp"
|
||||
android:height="768dp"
|
||||
android:viewportWidth="768"
|
||||
android:viewportHeight="768">
|
||||
<group
|
||||
android:name="wrapper"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="clippath"
|
||||
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z" />
|
||||
<group android:name="group">
|
||||
<group android:name="group_1">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#ED0021"
|
||||
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
|
||||
android:strokeWidth="1" />
|
||||
<group
|
||||
android:name="group_12"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:fillColor="#D40037"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group android:name="group_2">
|
||||
<group android:name="group_7">
|
||||
<group android:name="group_10">
|
||||
<group
|
||||
android:name="group_11"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:fillColor="#A70060"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
<clip-path
|
||||
android:name="mask_2"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_13"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="mask_1"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z" />
|
||||
<group
|
||||
android:name="group_9"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:fillColor="#BF005E"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_6"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-5"
|
||||
android:scaleX="1.2"
|
||||
android:scaleY="1.2" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_8"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<group
|
||||
android:name="group_14"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440">
|
||||
<path
|
||||
android:name="path_4"
|
||||
android:fillColor="#C70051"
|
||||
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_3"
|
||||
android:translateX="-360">
|
||||
<path
|
||||
android:name="path_6"
|
||||
android:fillColor="#251528"
|
||||
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_4"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384"
|
||||
android:scaleX="1.5"
|
||||
android:scaleY="1.5">
|
||||
<path
|
||||
android:name="path_5"
|
||||
android:fillColor="#251528"
|
||||
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_5"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384"
|
||||
android:rotation="-15"
|
||||
android:scaleX="3"
|
||||
android:scaleY="3">
|
||||
<path
|
||||
android:name="path_7"
|
||||
android:fillAlpha="0"
|
||||
android:fillColor="#FFD8DF"
|
||||
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="wrapper">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_6">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="-10"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_3">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="400"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="translateX"
|
||||
android:startOffset="250"
|
||||
android:valueFrom="-360"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_4">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="400"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="400"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_7">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_5">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="-45"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_8">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_9">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_11">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_12">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_13">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_14">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="200"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="5"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="250"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="5"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
4
app/src/alpha/res/values/strings.xml
Normal file
4
app/src/alpha/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dantotsu α</string>
|
||||
</resources>
|
||||
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"?>
|
||||
<resources>
|
||||
<string name="app_name">Dantotsu α</string>
|
||||
<string name="app_name">Dantotsu β</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
class CrashlyticsFactory {
|
||||
companion object {
|
||||
fun createCrashlytics(): CrashlyticsInterface {
|
||||
return CrashlyticsStub()
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
Normal file
9
app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.others
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
class CrashlyticsFactory {
|
||||
companion object {
|
||||
fun createCrashlytics(): CrashlyticsInterface {
|
||||
return FirebaseCrashlytics()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.FirebaseApp
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.google.firebase.ktx.app
|
||||
|
||||
class FirebaseCrashlytics : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
FirebaseApp.initializeApp(context)
|
||||
}
|
||||
override fun logException(e: Throwable) {
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
|
||||
override fun log(message: String) {
|
||||
FirebaseCrashlytics.getInstance().log(message)
|
||||
}
|
||||
|
||||
override fun setUserId(id: String) {
|
||||
Firebase.crashlytics.setUserId(id)
|
||||
}
|
||||
|
||||
override fun setCustomKey(key: String, value: String) {
|
||||
FirebaseCrashlytics.getInstance().setCustomKey(key, value)
|
||||
}
|
||||
|
||||
override fun setCrashlyticsCollectionEnabled(enabled: Boolean) {
|
||||
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,10 +10,12 @@ import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -28,41 +30,49 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun check(activity: FragmentActivity, post:Boolean=false) {
|
||||
if(post) snackString(currContext()?.getString(R.string.checking_for_update))
|
||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||
if (post) snackString(currContext()?.getString(R.string.checking_for_update))
|
||||
val repo = activity.getString(R.string.repo)
|
||||
tryWithSuspend {
|
||||
val (md, version) = if(BuildConfig.DEBUG){
|
||||
val (md, version) = if (BuildConfig.DEBUG) {
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
val r = res.filter { it.prerelease }.maxByOrNull {
|
||||
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v","")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
}else{
|
||||
} else {
|
||||
val res =
|
||||
client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
|
||||
res to res.substringAfter("# ").substringBefore("\n")
|
||||
}
|
||||
|
||||
logger("Git Version : $version")
|
||||
val dontShow = loadData("dont_ask_for_update_$version") ?: false
|
||||
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
|
||||
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
setTitleText("${if (BuildConfig.DEBUG) "Beta " else ""}Update " + currContext()!!.getString(R.string.available))
|
||||
setTitleText(
|
||||
"${if (BuildConfig.DEBUG) "Beta " else ""}Update " + currContext()!!.getString(
|
||||
R.string.available
|
||||
)
|
||||
)
|
||||
addView(
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
setCheck(currContext()!!.getString(R.string.dont_show_again, version), false) { isChecked ->
|
||||
setCheck(
|
||||
currContext()!!.getString(R.string.dont_show_again, version),
|
||||
false
|
||||
) { isChecked ->
|
||||
if (isChecked) {
|
||||
saveData("dont_ask_for_update_$version", true)
|
||||
PrefManager.setCustomVal("dont_ask_for_update_$version", true)
|
||||
}
|
||||
}
|
||||
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||
@@ -70,11 +80,11 @@ object AppUpdater {
|
||||
try {
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<GithubResponse>().assets?.find {
|
||||
it.browserDownloadURL.endsWith("apk")
|
||||
}?.browserDownloadURL.apply {
|
||||
if (this != null) activity.downloadUpdate(version, this)
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
it.browserDownloadURL.endsWith("apk")
|
||||
}?.browserDownloadURL.apply {
|
||||
if (this != null) activity.downloadUpdate(version, this)
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
@@ -87,24 +97,24 @@ object AppUpdater {
|
||||
show(activity.supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(post) snackString(currContext()?.getString(R.string.no_update_found))
|
||||
else {
|
||||
if (post) snackString(currContext()?.getString(R.string.no_update_found))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun compareVersion(version: String): Boolean {
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
if (BuildConfig.DEBUG) {
|
||||
return BuildConfig.VERSION_NAME != version
|
||||
else {
|
||||
} else {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull()?: 0.0
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
}
|
||||
@@ -141,7 +151,8 @@ object AppUpdater {
|
||||
-1
|
||||
}
|
||||
if (id == -1L) return true
|
||||
registerReceiver(
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
object : BroadcastReceiver() {
|
||||
@SuppressLint("Range")
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -170,12 +181,13 @@ object AppUpdater {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
fun openApk(context: Context, uri: Uri) {
|
||||
private fun openApk(context: Context, uri: Uri) {
|
||||
try {
|
||||
uri.path?.let {
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
@@ -206,7 +218,7 @@ object AppUpdater {
|
||||
val tagName: String,
|
||||
val prerelease: Boolean,
|
||||
@SerialName("created_at")
|
||||
val createdAt : String,
|
||||
val createdAt: String,
|
||||
val body: String? = null,
|
||||
val assets: List<Asset>? = null
|
||||
) {
|
||||
@@ -10,6 +10,9 @@
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
@@ -17,23 +20,22 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<queries>
|
||||
@@ -46,22 +48,37 @@
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_banner_foreground"
|
||||
android:icon="${icon_placeholder}"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="${icon_placeholder_round}"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Dantotsu"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup"
|
||||
android:banner="@drawable/ic_banner_foreground">
|
||||
tools:ignore="AllowBackup">
|
||||
<receiver
|
||||
android:name=".widgets.CurrentlyAiringWidget"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/currently_airing_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".subcriptions.NotificationClickReceiver" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity"
|
||||
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" >
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/epub+zip" />
|
||||
@@ -69,13 +86,11 @@
|
||||
<data android:mimeType="application/vnd.amazon.ebook" />
|
||||
<data android:mimeType="application/fb2+zip" />
|
||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||
|
||||
<data android:pathPattern=".*\\.epub" />
|
||||
<data android:pathPattern=".*\\.mobi" />
|
||||
<data android:pathPattern=".*\\.kf8" />
|
||||
<data android:pathPattern=".*\\.fb2" />
|
||||
<data android:pathPattern=".*\\.cbz" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
@@ -101,9 +116,9 @@
|
||||
<activity
|
||||
android:name=".media.CalendarActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name="ani.dantotsu.media.user.ListActivity" />
|
||||
<activity android:name=".media.user.ListActivity" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity"
|
||||
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="@string/manga"
|
||||
@@ -116,7 +131,7 @@
|
||||
<activity android:name=".media.CharacterDetailsActivity" />
|
||||
<activity android:name=".home.NoInternet" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.anime.ExoplayerView"
|
||||
android:name=".media.anime.ExoplayerView"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -125,7 +140,7 @@
|
||||
android:supportsPictureInPicture="true"
|
||||
tools:targetApi="n" />
|
||||
<activity
|
||||
android:name="ani.dantotsu.connections.anilist.Login"
|
||||
android:name=".connections.anilist.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -142,7 +157,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="ani.dantotsu.connections.mal.Login"
|
||||
android:name=".connections.mal.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -158,8 +173,8 @@
|
||||
android:scheme="dantotsu" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="ani.dantotsu.connections.discord.Login"
|
||||
<activity
|
||||
android:name=".connections.discord.Login"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -170,15 +185,32 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="dantotsu"/>
|
||||
<data android:scheme="dantotsu" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="discord.dantotsu.com"/>
|
||||
<data android:host="discord.dantotsu.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="ani.dantotsu.connections.anilist.UrlMedia"
|
||||
android:name=".others.webview.CookieCatcher"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter android:label="Discord Login for Dantotsu">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="dantotsu" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="discord.dantotsu.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".connections.anilist.UrlMedia"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
@@ -214,29 +246,30 @@
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<receiver
|
||||
android:name=".subcriptions.AlarmReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="Aani.dantotsu.ACTION_ALARM"/>
|
||||
<action android:name="Aani.dantotsu.ACTION_ALARM" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
@@ -255,18 +288,52 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<service android:name=".download.video.MyDownloadService"
|
||||
android:exported="false">
|
||||
<service
|
||||
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name=".download.video.ExoplayerDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".download.manga.MangaDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".download.novel.NovelDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".download.anime.AnimeDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".connections.discord.DiscordService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/ic_launcher_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,19 +8,36 @@ import androidx.multidex.MultiDex
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.others.DisabledReports
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class App : MultiDexApplication() {
|
||||
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||
private lateinit var novelExtensionManager: NovelExtensionManager
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
@@ -34,26 +51,68 @@ class App : MultiDexApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
|
||||
if(useMaterialYou) {
|
||||
|
||||
PrefManager.init(this)
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(PreferenceModule(this))
|
||||
|
||||
val crashlytics = Injekt.get<CrashlyticsInterface>()
|
||||
crashlytics.initialize(this)
|
||||
|
||||
val useMaterialYou: Boolean = PrefManager.getVal(PrefName.UseMaterialYou)
|
||||
if (useMaterialYou) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||
|
||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
initializeNetwork(baseContext)
|
||||
crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
(PrefManager.getVal(PrefName.SharedUserID) as Boolean).let {
|
||||
if (!it) return@let
|
||||
val dUsername = PrefManager.getVal(PrefName.DiscordUserName, null as String?)
|
||||
val aUsername = PrefManager.getVal(PrefName.AnilistUserName, null as String?)
|
||||
if (dUsername != null) {
|
||||
crashlytics.setCustomKey("dUsername", dUsername)
|
||||
}
|
||||
if (aUsername != null) {
|
||||
crashlytics.setCustomKey("aUsername", aUsername)
|
||||
}
|
||||
}
|
||||
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(PreferenceModule(this))
|
||||
|
||||
|
||||
initializeNetwork(baseContext)
|
||||
|
||||
setupNotificationChannels()
|
||||
if (!LogcatLogger.isInstalled) {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
}
|
||||
|
||||
animeExtensionManager = Injekt.get()
|
||||
mangaExtensionManager = Injekt.get()
|
||||
novelExtensionManager = Injekt.get()
|
||||
|
||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||
mangaScope.launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
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() {
|
||||
try {
|
||||
Notifications.createChannels(this)
|
||||
@@ -81,7 +140,7 @@ class App : MultiDexApplication() {
|
||||
|
||||
companion object {
|
||||
private var instance: App? = null
|
||||
var context : Context? = null
|
||||
var context: Context? = null
|
||||
fun currentContext(): Context? {
|
||||
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
||||
}
|
||||
|
||||
@@ -4,15 +4,19 @@ import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources.getSystem
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.Manifest
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities.*
|
||||
@@ -23,11 +27,16 @@ import android.telephony.TelephonyManager
|
||||
import android.text.InputFilter
|
||||
import android.text.Spanned
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.animation.*
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
@@ -40,10 +49,15 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||
import ani.dantotsu.connections.anilist.Genre
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||
import ani.dantotsu.subcriptions.NotificationClickReceiver
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
||||
@@ -53,8 +67,11 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.internal.ViewUtils
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import kotlinx.coroutines.*
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.*
|
||||
import java.lang.Runnable
|
||||
import java.lang.reflect.Field
|
||||
@@ -96,59 +113,27 @@ fun logger(e: Any?, print: Boolean = true) {
|
||||
println(e)
|
||||
}
|
||||
|
||||
fun saveData(fileName: String, data: Any?, context: Context? = null) {
|
||||
tryWith {
|
||||
val a = context ?: currContext()
|
||||
if (a != null) {
|
||||
val fos: FileOutputStream = a.openFileOutput(fileName, Context.MODE_PRIVATE)
|
||||
val os = ObjectOutputStream(fos)
|
||||
os.writeObject(data)
|
||||
os.close()
|
||||
fos.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = true): T? {
|
||||
val a = context ?: currContext()
|
||||
try {
|
||||
if (a?.fileList() != null)
|
||||
if (fileName in a.fileList()) {
|
||||
val fileIS: FileInputStream = a.openFileInput(fileName)
|
||||
val objIS = ObjectInputStream(fileIS)
|
||||
val data = objIS.readObject() as T
|
||||
objIS.close()
|
||||
fileIS.close()
|
||||
return data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun initActivity(a: Activity) {
|
||||
val window = a.window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false) ?: UserInterfaceSettings().apply {
|
||||
saveData("ui_settings", this)
|
||||
}
|
||||
uiSettings.darkMode.apply {
|
||||
val darkMode = PrefManager.getVal<Int>(PrefName.DarkMode)
|
||||
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
||||
darkMode.apply {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (this) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
2 -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
1 -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
}
|
||||
if (uiSettings.immersiveMode) {
|
||||
if (immersiveMode) {
|
||||
if (navBarHeight == 0) {
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))?.apply {
|
||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||
}
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||
?.apply {
|
||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
||||
}
|
||||
}
|
||||
a.hideStatusBar()
|
||||
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
|
||||
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) {
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
statusBarHeight = insets.top
|
||||
@@ -188,10 +174,21 @@ fun Activity.hideStatusBar() {
|
||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val window = dialog?.window
|
||||
val decorView: View = window?.decorView ?: return
|
||||
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
val typedValue = TypedValue()
|
||||
val theme = requireContext().theme
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSurface,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
window.navigationBarColor = typedValue.data
|
||||
}
|
||||
|
||||
override fun show(manager: FragmentManager, tag: String?) {
|
||||
@@ -202,25 +199,24 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
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 {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return@tryWith if (cap != null) {
|
||||
when {
|
||||
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
||||
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
||||
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
||||
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
||||
cap.hasTransport(TRANSPORT_USB) ||
|
||||
cap.hasTransport(TRANSPORT_VPN) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return@tryWith if (cap != null) {
|
||||
when {
|
||||
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
||||
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
||||
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
||||
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
||||
cap.hasTransport(TRANSPORT_USB) ||
|
||||
cap.hasTransport(TRANSPORT_VPN) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
} else true
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
} ?: false
|
||||
}
|
||||
|
||||
@@ -238,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 {
|
||||
var dialog: DatePickerDialog
|
||||
|
||||
@@ -263,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 {
|
||||
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 {
|
||||
val input = (dest.toString() + source.toString()).toDouble()
|
||||
if (isInRange(min, max, input)) return null
|
||||
@@ -288,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) {
|
||||
if (position == 0.0f && uiSettings.layoutAnimations) {
|
||||
setAnimation(view.context, view, uiSettings, 300, floatArrayOf(1.3f, 1f, 1.3f, 1f), 0.5f to 0f)
|
||||
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f).setDuration((200 * uiSettings.animationSpeed).toLong()).start()
|
||||
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
setAnimation(
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,12 +317,11 @@ class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : Vi
|
||||
fun setAnimation(
|
||||
context: Context,
|
||||
viewToAnimate: View,
|
||||
uiSettings: UserInterfaceSettings,
|
||||
duration: Long = 150,
|
||||
list: FloatArray = floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f),
|
||||
pivot: Pair<Float, Float> = 0.5f to 0.5f
|
||||
) {
|
||||
if (uiSettings.layoutAnimations) {
|
||||
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
val anim = ScaleAnimation(
|
||||
list[0],
|
||||
list[1],
|
||||
@@ -316,7 +332,7 @@ fun setAnimation(
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
pivot.second
|
||||
)
|
||||
anim.duration = (duration * uiSettings.animationSpeed).toLong()
|
||||
anim.duration = (duration * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
|
||||
anim.setInterpolator(context, R.anim.over_shoot)
|
||||
viewToAnimate.startAnimation(anim)
|
||||
}
|
||||
@@ -327,7 +343,11 @@ class FadingEdgeRecyclerView : RecyclerView {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
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 {
|
||||
return !clipToPadding
|
||||
@@ -413,13 +433,18 @@ fun MutableList<ShowResponse>.sortByTitle(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 }
|
||||
}
|
||||
|
||||
fun ImageView.loadImage(url: String?, size: Int = 0) {
|
||||
if (!url.isNullOrEmpty()) {
|
||||
loadImage(FileUrl(url), size)
|
||||
val localFile = File(url)
|
||||
if (localFile.exists()) {
|
||||
loadLocalImage(localFile, size)
|
||||
} else {
|
||||
loadImage(FileUrl(url), size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +452,17 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||
if (file?.url?.isNotEmpty() == true) {
|
||||
tryWith {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size).into(this)
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
||||
if (file?.exists() == true) {
|
||||
tryWith {
|
||||
Glide.with(this.context).load(file).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,7 +520,12 @@ abstract class GesturesListener : GestureDetector.SimpleOnGestureListener() {
|
||||
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)
|
||||
onScrollXClick(distanceX)
|
||||
return super.onScroll(e1, e2, distanceX, distanceY)
|
||||
@@ -545,18 +585,117 @@ fun openLinkInBrowser(link: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
|
||||
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"$APPLICATION_ID.provider",
|
||||
saveImage(
|
||||
bitmap,
|
||||
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS,
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||
title
|
||||
) ?: return
|
||||
)
|
||||
}
|
||||
|
||||
fun savePrefsToDownloads(
|
||||
title: String,
|
||||
serialized: String,
|
||||
context: Activity,
|
||||
password: CharArray? = null
|
||||
) {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"$APPLICATION_ID.provider",
|
||||
if (password != null) {
|
||||
savePrefs(
|
||||
serialized,
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||
title,
|
||||
context,
|
||||
password
|
||||
) ?: return
|
||||
} else {
|
||||
savePrefs(
|
||||
serialized,
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
|
||||
title,
|
||||
context
|
||||
) ?: return
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
|
||||
var file = File(path, "$title.ani")
|
||||
var counter = 1
|
||||
while (file.exists()) {
|
||||
file = File(path, "${title}_${counter}.ani")
|
||||
counter++
|
||||
}
|
||||
|
||||
return try {
|
||||
file.writeText(serialized)
|
||||
scanFile(file.absolutePath, context)
|
||||
toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath)))
|
||||
file
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to save settings: ${e.localizedMessage}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun savePrefs(
|
||||
serialized: String,
|
||||
path: String,
|
||||
title: String,
|
||||
context: Context,
|
||||
password: CharArray
|
||||
): File? {
|
||||
var file = File(path, "$title.sani")
|
||||
var counter = 1
|
||||
while (file.exists()) {
|
||||
file = File(path, "${title}_${counter}.sani")
|
||||
counter++
|
||||
}
|
||||
|
||||
val salt = generateSalt()
|
||||
|
||||
return try {
|
||||
val encryptedData = PreferenceKeystore.encryptWithPassword(password, serialized, salt)
|
||||
|
||||
// Combine salt and encrypted data
|
||||
val dataToSave = salt + encryptedData
|
||||
|
||||
file.writeBytes(dataToSave)
|
||||
scanFile(file.absolutePath, context)
|
||||
toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath)))
|
||||
file
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to save settings: ${e.localizedMessage}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
val requiredPermissions = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
||||
}.toTypedArray()
|
||||
|
||||
return if (requiredPermissions.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
|
||||
|
||||
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
@@ -574,13 +713,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||
|
||||
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
||||
val imageFile = File(path, "$imageFileName.png")
|
||||
return tryWith {
|
||||
return try {
|
||||
val fOut: OutputStream = FileOutputStream(imageFile)
|
||||
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
|
||||
fOut.close()
|
||||
scanFile(imageFile.absolutePath, currContext()!!)
|
||||
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
|
||||
imageFile
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to save image: ${e.localizedMessage}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,13 +765,19 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun countDown(media: Media, view: ViewGroup) {
|
||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) {
|
||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
|
||||
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
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) {
|
||||
val a = millisUntilFinished / 1000
|
||||
v.mediaCountdown.text = currActivity()?.getString(
|
||||
@@ -664,10 +812,11 @@ fun MutableMap<String, Genre>.checkGenreTime(genre: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun setSlideIn(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
||||
if (uiSettings.layoutAnimations) {
|
||||
fun setSlideIn() = AnimationSet(false).apply {
|
||||
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
var animation: Animation = AlphaAnimation(0.0f, 1.0f)
|
||||
animation.duration = (500 * uiSettings.animationSpeed).toLong()
|
||||
val animationSpeed: Float = PrefManager.getVal(PrefName.AnimationSpeed)
|
||||
animation.duration = (500 * animationSpeed).toLong()
|
||||
animation.interpolator = AccelerateDecelerateInterpolator()
|
||||
addAnimation(animation)
|
||||
|
||||
@@ -678,16 +827,17 @@ fun setSlideIn(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
||||
Animation.RELATIVE_TO_SELF, 0f
|
||||
)
|
||||
|
||||
animation.duration = (750 * uiSettings.animationSpeed).toLong()
|
||||
animation.duration = (750 * animationSpeed).toLong()
|
||||
animation.interpolator = OvershootInterpolator(1.1f)
|
||||
addAnimation(animation)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSlideUp(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
||||
if (uiSettings.layoutAnimations) {
|
||||
fun setSlideUp() = AnimationSet(false).apply {
|
||||
if (PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
var animation: Animation = AlphaAnimation(0.0f, 1.0f)
|
||||
animation.duration = (500 * uiSettings.animationSpeed).toLong()
|
||||
val animationSpeed: Float = PrefManager.getVal(PrefName.AnimationSpeed)
|
||||
animation.duration = (500 * animationSpeed).toLong()
|
||||
animation.interpolator = AccelerateDecelerateInterpolator()
|
||||
addAnimation(animation)
|
||||
|
||||
@@ -698,7 +848,7 @@ fun setSlideUp(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
|
||||
Animation.RELATIVE_TO_SELF, 0f
|
||||
)
|
||||
|
||||
animation.duration = (750 * uiSettings.animationSpeed).toLong()
|
||||
animation.duration = (750 * animationSpeed).toLong()
|
||||
animation.interpolator = OvershootInterpolator(1.1f)
|
||||
addAnimation(animation)
|
||||
}
|
||||
@@ -720,41 +870,52 @@ fun toast(string: String?) {
|
||||
if (string != null) {
|
||||
logger(string)
|
||||
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) {
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG)
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
width = WRAP_CONTENT
|
||||
}
|
||||
translationY = -(navBarHeight.dp + 32f)
|
||||
translationZ = 32f
|
||||
updatePadding(16f.px, right = 16f.px)
|
||||
setOnClickListener {
|
||||
snackBar.dismiss()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
copyToClipboard(clipboard ?: s, false)
|
||||
toast(getString(R.string.copied_to_clipboard))
|
||||
true
|
||||
try { //I have no idea why this sometimes crashes for some people...
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(
|
||||
window.decorView.findViewById(android.R.id.content),
|
||||
s,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
width = WRAP_CONTENT
|
||||
}
|
||||
translationY = -(navBarHeight.dp + 32f)
|
||||
translationZ = 32f
|
||||
updatePadding(16f.px, right = 16f.px)
|
||||
setOnClickListener {
|
||||
snackBar.dismiss()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
copyToClipboard(clipboard ?: s, false)
|
||||
toast(getString(R.string.copied_to_clipboard))
|
||||
true
|
||||
}
|
||||
}
|
||||
snackBar.show()
|
||||
}
|
||||
snackBar.show()
|
||||
}
|
||||
logger(s)
|
||||
}
|
||||
logger(s)
|
||||
} catch (e: Exception) {
|
||||
logger(e.stackTraceToString())
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
|
||||
@@ -775,16 +936,21 @@ class SpinnerNoSwipe : androidx.appcompat.widget.AppCompatSpinner {
|
||||
setup()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
) {
|
||||
setup()
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
return performClick()
|
||||
}
|
||||
})
|
||||
mGestureDetector =
|
||||
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
return performClick()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
@@ -828,7 +994,11 @@ fun getCurrentBrightnessValue(context: Context): 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)
|
||||
@@ -850,12 +1020,38 @@ fun checkCountry(context: Context): Boolean {
|
||||
tz.equals("Asia/Kolkata", ignoreCase = true)
|
||||
}
|
||||
|
||||
TelephonyManager.SIM_STATE_READY -> {
|
||||
TelephonyManager.SIM_STATE_READY -> {
|
||||
val countryCodeValue = telMgr.networkCountryIso
|
||||
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.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
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.ColorDrawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -22,12 +16,12 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@@ -35,12 +29,14 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
@@ -48,66 +44,99 @@ import ani.dantotsu.home.MangaFragment
|
||||
import ani.dantotsu.home.NoInternet
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.Serializable
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
|
||||
private val scope = lifecycleScope
|
||||
private var load = false
|
||||
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
|
||||
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
|
||||
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
//get FRAGMENT_CLASS_NAME from intent
|
||||
val fragment = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
val backgroundDrawable = bottomBar.background as GradientDrawable
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
bottomBar.background = backgroundDrawable
|
||||
_bottomBar.background = backgroundDrawable
|
||||
}
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
|
||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
val offset = try {
|
||||
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
resources.getDimensionPixelSize(statusBarHeightId)
|
||||
} catch (e: Exception) {
|
||||
statusBarHeight
|
||||
}
|
||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||
mangaScope.launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
|
||||
layoutParams.topMargin = 11 * offset / 12
|
||||
binding.incognito.layoutParams = layoutParams
|
||||
incognitoLiveData = PrefManager.getLiveVal(
|
||||
PrefName.Incognito,
|
||||
false
|
||||
).asLiveBool()
|
||||
incognitoLiveData.observe(this) {
|
||||
if (it) {
|
||||
val slideDownAnim = ObjectAnimator.ofFloat(
|
||||
binding.incognito,
|
||||
View.TRANSLATION_Y,
|
||||
-(binding.incognito.height.toFloat() + statusBarHeight),
|
||||
0f
|
||||
)
|
||||
slideDownAnim.duration = 200
|
||||
slideDownAnim.start()
|
||||
binding.incognito.visibility = View.VISIBLE
|
||||
} else {
|
||||
val slideUpAnim = ObjectAnimator.ofFloat(
|
||||
binding.incognito,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-(binding.incognito.height.toFloat() + statusBarHeight)
|
||||
)
|
||||
slideUpAnim.duration = 200
|
||||
slideUpAnim.start()
|
||||
//wait for animation to finish
|
||||
Handler(Looper.getMainLooper()).postDelayed(
|
||||
{ binding.incognito.visibility = View.GONE },
|
||||
200
|
||||
)
|
||||
}
|
||||
}
|
||||
incognitoNotification(this)
|
||||
|
||||
var doubleBackToExitPressedOnce = false
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
@@ -122,138 +151,200 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
val preferences: SourcePreferences = Injekt.get()
|
||||
if (preferences.animeExtensionUpdatesCount()
|
||||
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
||||
) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"You have extension updates available!",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
binding.root.isMotionEventSplittingEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||
binding.root.addView(splash.root)
|
||||
(splash.splashImage.drawable as Animatable).start()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||
binding.root.addView(splash.root)
|
||||
(splash.splashImage.drawable as Animatable).start()
|
||||
|
||||
// Wait for 2 seconds (2000 milliseconds)
|
||||
delay(2000)
|
||||
delay(1200)
|
||||
|
||||
// Now perform the animation
|
||||
ObjectAnimator.ofFloat(
|
||||
splash.root,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splash.root.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { binding.root.removeView(splash.root) }
|
||||
start()
|
||||
ObjectAnimator.ofFloat(
|
||||
splash.root,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splash.root.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { binding.root.removeView(splash.root) }
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
splashScreen.setOnExitAnimationListener { splashScreenView ->
|
||||
ObjectAnimator.ofFloat(
|
||||
splashScreenView,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splashScreenView.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { splashScreenView.remove() }
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
binding.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
selectedOption = if (fragment != null) {
|
||||
when (fragment) {
|
||||
AnimeFragment::class.java.name -> 0
|
||||
HomeFragment::class.java.name -> 1
|
||||
MangaFragment::class.java.name -> 2
|
||||
else -> 1
|
||||
}
|
||||
} else {
|
||||
PrefManager.getVal(PrefName.DefaultStartUpTab)
|
||||
}
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
if (!isOnline(this)) {
|
||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMain(this@MainActivity)
|
||||
val id = intent.extras?.getInt("mediaId", 0)
|
||||
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
||||
val cont = intent.extras?.getBoolean("continue") ?: false
|
||||
if (id != null && id != 0) {
|
||||
val media = withContext(Dispatchers.IO) {
|
||||
Anilist.query.getMedia(id, isMAL)
|
||||
}
|
||||
if (media != null) {
|
||||
media.cameFromContinue = cont
|
||||
startActivity(
|
||||
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
||||
.putExtra("media", media as Serializable)
|
||||
)
|
||||
} else {
|
||||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||
}
|
||||
}
|
||||
delay(500)
|
||||
startSubscription()
|
||||
}
|
||||
load = true
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (loadData<Boolean>("allow_opening_links", this) != true) {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
||||
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
||||
addView(TextView(this@MainActivity).apply {
|
||||
val markWon =
|
||||
Markwon.builder(this@MainActivity)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
markWon.setMarkdown(this, md)
|
||||
})
|
||||
|
||||
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
||||
saveData("allow_opening_links", true, this@MainActivity)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
||||
saveData("allow_opening_links", true, this@MainActivity)
|
||||
tryWith(true) {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
if (offlineMode) {
|
||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) { it ->
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
selectedOption,
|
||||
false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
}
|
||||
}.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMain(this@MainActivity)
|
||||
val id = intent.extras?.getInt("mediaId", 0)
|
||||
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
||||
val cont = intent.extras?.getBoolean("continue") ?: false
|
||||
if (id != null && id != 0) {
|
||||
val media = withContext(Dispatchers.IO) {
|
||||
Anilist.query.getMedia(id, isMAL)
|
||||
}
|
||||
if (media != null) {
|
||||
media.cameFromContinue = cont
|
||||
startActivity(
|
||||
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
||||
.putExtra("media", media as Serializable)
|
||||
)
|
||||
} else {
|
||||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||
}
|
||||
}
|
||||
delay(500)
|
||||
startSubscription()
|
||||
}
|
||||
load = true
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (!(PrefManager.getVal(PrefName.AllowOpeningLinks) as Boolean)) {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
||||
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
||||
addView(TextView(this@MainActivity).apply {
|
||||
val markWon =
|
||||
Markwon.builder(this@MainActivity)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
markWon.setMarkdown(this, md)
|
||||
})
|
||||
|
||||
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
||||
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
||||
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
|
||||
tryWith(true) {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: Remove this
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||
val downloadCursor = index.getDownloads()
|
||||
while (downloadCursor.moveToNext()) {
|
||||
val download = downloadCursor.download
|
||||
Log.e("Downloader", download.request.uri.toString())
|
||||
Log.e("Downloader", download.request.id)
|
||||
Log.e("Downloader", download.request.mimeType.toString())
|
||||
Log.e("Downloader", download.request.data.size.toString())
|
||||
Log.e("Downloader", download.bytesDownloaded.toString())
|
||||
Log.e("Downloader", download.state.toString())
|
||||
Log.e("Downloader", download.failureReason.toString())
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (download.state == Download.STATE_FAILED) { //simple cleanup
|
||||
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -273,4 +364,4 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,58 +8,51 @@ import ani.dantotsu.others.webview.WebViewBottomDialog
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.nicehttp.addGenericDns
|
||||
import kotlinx.coroutines.*
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
import okhttp3.Cache
|
||||
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.Serializable
|
||||
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.KFunction
|
||||
|
||||
val defaultHeaders = mapOf(
|
||||
"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 defaultHeaders: Map<String, String>
|
||||
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
lateinit var client: Requests
|
||||
|
||||
fun initializeNetwork(context: Context) {
|
||||
val dns = loadData<Int>("settings_dns")
|
||||
cache = Cache(
|
||||
File(context.cacheDir, "http_cache"),
|
||||
5 * 1024L * 1024L // 5 MiB
|
||||
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
||||
defaultHeaders = mapOf(
|
||||
"User-Agent" to
|
||||
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||
)
|
||||
okHttpClient = OkHttpClient.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.cache(cache)
|
||||
.apply {
|
||||
when (dns) {
|
||||
1 -> addGoogleDns()
|
||||
2 -> addCloudFlareDns()
|
||||
3 -> addAdGuardDns()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
okHttpClient = networkHelper.client
|
||||
client = Requests(
|
||||
okHttpClient,
|
||||
networkHelper.client,
|
||||
defaultHeaders,
|
||||
defaultCacheTime = 6,
|
||||
defaultCacheTimeUnit = TimeUnit.HOURS,
|
||||
responseParser = Mapper
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
call.invoke()
|
||||
} catch (e: Throwable) {
|
||||
@@ -202,28 +199,29 @@ fun OkHttpClient.Builder.addAdGuardDns() = (
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, String>? {
|
||||
var map : Map<String,String>? = null
|
||||
var map: Map<String, String>? = null
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
webViewDialog.callback = {
|
||||
map = it
|
||||
latch.countDown()
|
||||
}
|
||||
val fragmentManager = (currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
|
||||
val fragmentManager =
|
||||
(currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
|
||||
webViewDialog.show(fragmentManager, "web-view")
|
||||
delay(0)
|
||||
latch.await(2,TimeUnit.MINUTES)
|
||||
latch.await(2, TimeUnit.MINUTES)
|
||||
return map
|
||||
}
|
||||
|
||||
suspend fun webViewInterface(type: String, url: FileUrl): Map<String, String>? {
|
||||
val webViewDialog: WebViewBottomDialog = when (type) {
|
||||
"Cloudflare" -> CloudFlare.newInstance(url)
|
||||
else -> return null
|
||||
else -> return null
|
||||
}
|
||||
return webViewInterface(webViewDialog)
|
||||
}
|
||||
|
||||
suspend fun webViewInterface(type: String, url: String): Map<String, String>? {
|
||||
return webViewInterface(type,FileUrl(url))
|
||||
return webViewInterface(type, FileUrl(url))
|
||||
}
|
||||
@@ -2,17 +2,26 @@ package ani.dantotsu.aniyomi.anime.custom
|
||||
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingleton
|
||||
@@ -20,17 +29,20 @@ import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { NetworkHelper(app, get()) }
|
||||
addSingletonFactory { DownloadsManager(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app) }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
|
||||
addSingletonFactory { MangaExtensionManager(app) }
|
||||
addSingletonFactory { NovelExtensionManager(app) }
|
||||
|
||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
addSingleton(sharedPreferences)
|
||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||
|
||||
addSingletonFactory {
|
||||
Json {
|
||||
@@ -39,7 +51,18 @@ class AppModule(val app: Application) : InjektModule {
|
||||
}
|
||||
}
|
||||
|
||||
addSingletonFactory { StandaloneDatabaseProvider(app) }
|
||||
|
||||
addSingletonFactory<CrashlyticsInterface> {
|
||||
ani.dantotsu.connections.crashlytics.CrashlyticsFactory.createCrashlytics()
|
||||
}
|
||||
|
||||
addSingletonFactory { MangaCache() }
|
||||
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
get<AnimeSourceManager>()
|
||||
get<MangaSourceManager>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,13 +72,6 @@ class PreferenceModule(val application: Application) : InjektModule {
|
||||
AndroidPreferenceStore(application)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
NetworkPreferences(
|
||||
preferenceStore = get(),
|
||||
verboseLogging = false,
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
SourcePreferences(get())
|
||||
}
|
||||
|
||||
@@ -6,34 +6,40 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun updateProgress(media: Media, number: String) {
|
||||
if (Anilist.userid != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val a = number.toFloatOrNull()?.roundToInt()
|
||||
if ((a?:0) > (media.userProgress?:0)) {
|
||||
Anilist.mutation.editList(
|
||||
media.id,
|
||||
a,
|
||||
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
||||
)
|
||||
MAL.query.editList(
|
||||
media.idMAL,
|
||||
media.anime != null,
|
||||
a, null,
|
||||
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
||||
)
|
||||
toast(currContext()?.getString(R.string.setting_progress, a))
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if (!incognito) {
|
||||
if (Anilist.userid != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val a = number.toFloatOrNull()?.toInt()
|
||||
if ((a ?: 0) > (media.userProgress ?: 0)) {
|
||||
Anilist.mutation.editList(
|
||||
media.id,
|
||||
a,
|
||||
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
||||
)
|
||||
MAL.query.editList(
|
||||
media.idMAL,
|
||||
media.anime != null,
|
||||
a, null,
|
||||
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
||||
)
|
||||
toast(currContext()?.getString(R.string.setting_progress, a))
|
||||
}
|
||||
media.userProgress = a
|
||||
Refresh.all()
|
||||
}
|
||||
media.userProgress = a
|
||||
Refresh.all()
|
||||
} else {
|
||||
toast(currContext()?.getString(R.string.login_anilist_account))
|
||||
}
|
||||
} 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.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
|
||||
object Anilist {
|
||||
val query: AnilistQueries = AnilistQueries()
|
||||
@@ -28,8 +31,15 @@ object Anilist {
|
||||
var genres: ArrayList<String>? = null
|
||||
var tags: Map<Boolean, List<String>>? = null
|
||||
|
||||
var rateLimitReset: Long = 0
|
||||
|
||||
val sortBy = listOf(
|
||||
"SCORE_DESC","POPULARITY_DESC","TRENDING_DESC","TITLE_ENGLISH","TITLE_ENGLISH_DESC","SCORE"
|
||||
"SCORE_DESC",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"SCORE"
|
||||
)
|
||||
|
||||
val seasons = listOf(
|
||||
@@ -51,11 +61,11 @@ object Anilist {
|
||||
private val cal: Calendar = Calendar.getInstance()
|
||||
private val currentYear = cal.get(Calendar.YEAR)
|
||||
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
|
||||
0, 1, 2 -> 0
|
||||
3, 4, 5 -> 1
|
||||
6, 7, 8 -> 2
|
||||
0, 1, 2 -> 0
|
||||
3, 4, 5 -> 1
|
||||
6, 7, 8 -> 2
|
||||
9, 10, 11 -> 3
|
||||
else -> 0
|
||||
else -> 0
|
||||
}
|
||||
|
||||
private fun getSeason(next: Boolean): Pair<String, Int> {
|
||||
@@ -89,15 +99,12 @@ object Anilist {
|
||||
}
|
||||
}
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
if ("anilistToken" in context.fileList()) {
|
||||
token = File(context.filesDir, "anilistToken").readText()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
fun getSavedToken(): Boolean {
|
||||
token = PrefManager.getVal(PrefName.AnilistToken, null as String?)
|
||||
return !token.isNullOrEmpty()
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
fun removeSavedToken() {
|
||||
token = null
|
||||
username = null
|
||||
adult = false
|
||||
@@ -106,9 +113,7 @@ object Anilist {
|
||||
bg = null
|
||||
episodesWatched = null
|
||||
chapterRead = null
|
||||
if ("anilistToken" in context.fileList()) {
|
||||
File(context.filesDir, "anilistToken").delete()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.AnilistToken)
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> executeQuery(
|
||||
@@ -120,6 +125,10 @@ object Anilist {
|
||||
cache: Int? = null
|
||||
): T? {
|
||||
return tryWithSuspend {
|
||||
if (rateLimitReset > System.currentTimeMillis() / 1000) {
|
||||
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||
}
|
||||
val data = mapOf(
|
||||
"query" to query,
|
||||
"variables" to variables
|
||||
@@ -132,7 +141,24 @@ object Anilist {
|
||||
if (token != null || force) {
|
||||
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 (show) println("Response : ${json.text}")
|
||||
json.parsed()
|
||||
|
||||
@@ -20,7 +20,7 @@ class AnilistMutations {
|
||||
repeat: Int? = null,
|
||||
notes: String? = null,
|
||||
status: String? = null,
|
||||
private:Boolean? = null,
|
||||
private: Boolean? = null,
|
||||
startedAt: FuzzyDate? = null,
|
||||
completedAt: FuzzyDate? = null,
|
||||
customList: List<String>? = null
|
||||
@@ -41,7 +41,7 @@ class AnilistMutations {
|
||||
${if (repeat != null) ""","repeat":$repeat""" else ""}
|
||||
${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" 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(""" """, "")
|
||||
println(variables)
|
||||
executeQuery<JsonObject>(query, variables, show = true)
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Base64
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.checkGenreTime
|
||||
import ani.dantotsu.checkId
|
||||
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.Page
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.checkGenreTime
|
||||
import ani.dantotsu.checkId
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Author
|
||||
import ani.dantotsu.media.Character
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.Studio
|
||||
import ani.dantotsu.others.MalScraper
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class AnilistQueries {
|
||||
@@ -33,6 +39,8 @@ class AnilistQueries {
|
||||
}.also { println("time : $it") }
|
||||
val user = response?.data?.user ?: return false
|
||||
|
||||
PrefManager.setVal(PrefName.AnilistUserName, user.name)
|
||||
|
||||
Anilist.userid = user.id
|
||||
Anilist.username = user.name
|
||||
Anilist.bg = user.bannerImage
|
||||
@@ -113,9 +121,13 @@ class AnilistQueries {
|
||||
name = i.node?.name?.userPreferred,
|
||||
image = i.node?.image?.medium,
|
||||
banner = media.banner ?: media.cover,
|
||||
role = when (i.role.toString()){
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role) ?: "MAIN"
|
||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role) ?: "SUPPORTING"
|
||||
role = when (i.role.toString()) {
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||
?: "MAIN"
|
||||
|
||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
|
||||
?: "SUPPORTING"
|
||||
|
||||
else -> i.role.toString()
|
||||
}
|
||||
)
|
||||
@@ -129,11 +141,16 @@ class AnilistQueries {
|
||||
val m = Media(mediaEdge)
|
||||
media.relations?.add(m)
|
||||
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") {
|
||||
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 }
|
||||
@@ -199,17 +216,19 @@ class AnilistQueries {
|
||||
)
|
||||
}
|
||||
|
||||
media.anime.nextAiringEpisodeTime = fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
||||
media.anime.nextAiringEpisodeTime =
|
||||
fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
||||
|
||||
fetchedMedia.externalLinks?.forEach { i ->
|
||||
when (i.site.lowercase()) {
|
||||
"youtube" -> media.anime.youtube = i.url
|
||||
"crunchyroll" -> media.crunchySlug = i.url?.split("/")?.getOrNull(3)
|
||||
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
||||
"youtube" -> media.anime.youtube = i.url
|
||||
"crunchyroll" -> media.crunchySlug =
|
||||
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 {
|
||||
media.manga.author = Author(
|
||||
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.error_getting_data))
|
||||
if (currContext()?.let { isOnline(it) } == true) {
|
||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||
}
|
||||
}
|
||||
}
|
||||
val mal = async {
|
||||
@@ -241,15 +262,36 @@ class AnilistQueries {
|
||||
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 map = mutableMapOf<Int, Media>()
|
||||
val statuses = if(!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING")
|
||||
suspend fun repeat(status: String) {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """)
|
||||
val query = if (planned) {
|
||||
"""{ planned: ${continueMediaQuery(type, "PLANNING")} }"""
|
||||
} else {
|
||||
"""{
|
||||
current: ${continueMediaQuery(type, "CURRENT")},
|
||||
repeating: ${continueMediaQuery(type, "REPEATING")}
|
||||
}"""
|
||||
}
|
||||
|
||||
response?.data?.mediaListCollection?.lists?.forEach { li ->
|
||||
val response = executeQuery<Query.CombinedMediaListResponse>(query)
|
||||
if (planned) {
|
||||
response?.data?.planned?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response?.data?.current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
response?.data?.repeating?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
@@ -257,10 +299,8 @@ class AnilistQueries {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statuses.forEach { repeat(it) }
|
||||
val set = loadData<MutableSet<Int>>("continue_$type")
|
||||
if (set != null) {
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
if (map.containsKey(it)) returnArray.add(map[it]!!)
|
||||
}
|
||||
@@ -271,34 +311,40 @@ class AnilistQueries {
|
||||
return returnArray
|
||||
}
|
||||
|
||||
private fun continueMediaQuery(type: String, status: String): String {
|
||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
|
||||
}
|
||||
|
||||
suspend fun favMedia(anime: Boolean): ArrayList<Media> {
|
||||
var hasNextPage = true
|
||||
var page = 0
|
||||
|
||||
suspend fun getNextPage(page:Int): List<Media> {
|
||||
val response =
|
||||
executeQuery<Query.User>("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""")
|
||||
suspend fun getNextPage(page: Int): List<Media> {
|
||||
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page)}}""")
|
||||
val favourites = response?.data?.user?.favourites
|
||||
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
||||
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
||||
return apiMediaList?.edges?.mapNotNull {
|
||||
it.node?.let { i->
|
||||
it.node?.let { i ->
|
||||
Media(i).apply { isFav = true }
|
||||
}
|
||||
} ?: return listOf()
|
||||
}
|
||||
|
||||
val responseArray = arrayListOf<Media>()
|
||||
while(hasNextPage){
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
responseArray.addAll(getNextPage(page))
|
||||
}
|
||||
return responseArray
|
||||
}
|
||||
|
||||
private fun favMediaQuery(anime: Boolean, page: Int): String {
|
||||
return """User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
||||
}
|
||||
|
||||
suspend fun recommendations(): ArrayList<Media> {
|
||||
val response =
|
||||
executeQuery<Query.Page>(""" { Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } } """)
|
||||
val response = executeQuery<Query.Page>("""{${recommendationQuery()}}""")
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
response?.data?.page?.apply {
|
||||
recommendations?.onEach {
|
||||
@@ -314,7 +360,7 @@ class AnilistQueries {
|
||||
val types = arrayOf("ANIME", "MANGA")
|
||||
suspend fun repeat(type: String) {
|
||||
val res =
|
||||
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """)
|
||||
executeQuery<Query.MediaListCollection>("""{${recommendationPlannedQuery(type)}}""")
|
||||
res?.data?.mediaListCollection?.lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
@@ -332,8 +378,191 @@ class AnilistQueries {
|
||||
return list
|
||||
}
|
||||
|
||||
private fun recommendationQuery(): String {
|
||||
return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """
|
||||
}
|
||||
|
||||
private fun recommendationPlannedQuery(type: String): String {
|
||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
|
||||
}
|
||||
|
||||
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
|
||||
val toShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayoutShow) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations
|
||||
var query = """{"""
|
||||
if (toShow.getOrNull(0) == true) query += """currentAnime: ${
|
||||
continueMediaQuery(
|
||||
"ANIME",
|
||||
"CURRENT"
|
||||
)
|
||||
}, repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}"""
|
||||
if (toShow.getOrNull(1) == true) query += """favoriteAnime: ${favMediaQuery(true, 1)}"""
|
||||
if (toShow.getOrNull(2) == true) query += """plannedAnime: ${
|
||||
continueMediaQuery(
|
||||
"ANIME",
|
||||
"PLANNING"
|
||||
)
|
||||
}"""
|
||||
if (toShow.getOrNull(3) == true) query += """currentManga: ${
|
||||
continueMediaQuery(
|
||||
"MANGA",
|
||||
"CURRENT"
|
||||
)
|
||||
}, repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}"""
|
||||
if (toShow.getOrNull(4) == true) query += """favoriteManga: ${favMediaQuery(false, 1)}"""
|
||||
if (toShow.getOrNull(5) == true) query += """plannedManga: ${
|
||||
continueMediaQuery(
|
||||
"MANGA",
|
||||
"PLANNING"
|
||||
)
|
||||
}"""
|
||||
if (toShow.getOrNull(6) == true) query += """recommendationQuery: ${recommendationQuery()}, recommendationPlannedQueryAnime: ${
|
||||
recommendationPlannedQuery(
|
||||
"ANIME"
|
||||
)
|
||||
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
|
||||
query += """}""".trimEnd(',')
|
||||
|
||||
val response = executeQuery<Query.HomePageMedia>(query)
|
||||
val returnMap = mutableMapOf<String, ArrayList<Media>>()
|
||||
fun current(type: String) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val current =
|
||||
if (type == "Anime") response?.data?.currentAnime else response?.data?.currentManga
|
||||
val repeating =
|
||||
if (type == "Anime") response?.data?.repeatingAnime else response?.data?.repeatingManga
|
||||
current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
repeating?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_${type.uppercase()}", setOf())
|
||||
.toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
|
||||
}
|
||||
for (i in subMap) {
|
||||
if (i.value !in returnArray) returnArray.add(i.value)
|
||||
}
|
||||
} else returnArray.addAll(subMap.values)
|
||||
returnMap["current$type"] = returnArray
|
||||
|
||||
}
|
||||
|
||||
fun planned(type: String) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val current =
|
||||
if (type == "Anime") response?.data?.plannedAnime else response?.data?.plannedManga
|
||||
current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
|
||||
}
|
||||
for (i in subMap) {
|
||||
if (i.value !in returnArray) returnArray.add(i.value)
|
||||
}
|
||||
} else returnArray.addAll(subMap.values)
|
||||
returnMap["planned$type"] = returnArray
|
||||
}
|
||||
|
||||
fun favorite(type: String) {
|
||||
val favourites =
|
||||
if (type == "Anime") response?.data?.favoriteAnime?.favourites else response?.data?.favoriteManga?.favourites
|
||||
val apiMediaList = if (type == "Anime") favourites?.anime else favourites?.manga
|
||||
val returnArray = arrayListOf<Media>()
|
||||
apiMediaList?.edges?.forEach {
|
||||
it.node?.let { i ->
|
||||
returnArray.add(Media(i).apply { isFav = true })
|
||||
}
|
||||
}
|
||||
returnMap["favorite$type"] = returnArray
|
||||
}
|
||||
|
||||
if (toShow.getOrNull(0) == true) {
|
||||
current("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(1) == true) {
|
||||
favorite("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(2) == true) {
|
||||
planned("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(3) == true) {
|
||||
current("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(4) == true) {
|
||||
favorite("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(5) == true) {
|
||||
planned("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(6) == true) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
response?.data?.recommendationQuery?.apply {
|
||||
recommendations?.onEach {
|
||||
val json = it.mediaRecommendation
|
||||
if (json != null) {
|
||||
val m = Media(json)
|
||||
m.relation = json.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
response?.data?.recommendationPlannedQueryAnime?.apply {
|
||||
lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.status == "RELEASING" || m.status == "FINISHED") {
|
||||
m.relation = it.media?.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response?.data?.recommendationPlannedQueryManga?.apply {
|
||||
lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.status == "RELEASING" || m.status == "FINISHED") {
|
||||
m.relation = it.media?.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val list = ArrayList(subMap.values.toList())
|
||||
list.sortByDescending { it.meanScore }
|
||||
returnMap["recommendations"] = list
|
||||
}
|
||||
return returnMap
|
||||
}
|
||||
|
||||
|
||||
private suspend fun bannerImage(type: String): String? {
|
||||
var image = loadData<BannerImage>("banner_$type")
|
||||
//var image = loadData<BannerImage>("banner_$type")
|
||||
val image: BannerImage? = BannerImage(
|
||||
PrefManager.getCustomVal("banner_${type}_url", null),
|
||||
PrefManager.getCustomVal("banner_${type}_time", 0L)
|
||||
)
|
||||
if (image == null || image.checkTime()) {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
|
||||
@@ -344,13 +573,9 @@ class AnilistQueries {
|
||||
else null
|
||||
}
|
||||
}?.flatten()?.randomOrNull() ?: return null
|
||||
|
||||
image = BannerImage(
|
||||
random,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
saveData("banner_$type", image)
|
||||
return image.url
|
||||
PrefManager.setCustomVal("banner_${type}_url", random)
|
||||
PrefManager.setCustomVal("banner_${type}_time", System.currentTimeMillis())
|
||||
return random
|
||||
} else return image.url
|
||||
}
|
||||
|
||||
@@ -361,7 +586,11 @@ class AnilistQueries {
|
||||
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 =
|
||||
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>>()
|
||||
@@ -388,33 +617,55 @@ class AnilistQueries {
|
||||
if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!!
|
||||
}
|
||||
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"]?.sortWith(compareBy { it.userFavOrder })
|
||||
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
|
||||
sorted["Favourites"]?.forEach { fav ->
|
||||
all.find { it.id == fav.id }?.let {
|
||||
fav.userProgress = it.userProgress
|
||||
}
|
||||
}
|
||||
|
||||
sorted["All"] = all
|
||||
|
||||
val sort = sortOrder ?: options?.rowOrder
|
||||
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
||||
else PrefManager.getVal(PrefName.MangaListSortOrder)
|
||||
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
||||
for (i in sorted.keys) {
|
||||
when (sort) {
|
||||
"score" -> sorted[i]?.sortWith { b, a -> compareValuesBy(a, b, { it.userScore }, { it.meanScore }) }
|
||||
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
||||
"score" -> sorted[i]?.sortWith { b, a ->
|
||||
compareValuesBy(
|
||||
a,
|
||||
b,
|
||||
{ it.userScore },
|
||||
{ it.meanScore })
|
||||
}
|
||||
|
||||
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
||||
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
|
||||
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
||||
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
||||
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
||||
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
||||
}
|
||||
}
|
||||
return sorted
|
||||
}
|
||||
|
||||
|
||||
suspend fun getGenresAndTags(activity: Activity): Boolean {
|
||||
var genres: ArrayList<String>? = loadData("genres_list", activity)
|
||||
var tags: Map<Boolean, List<String>>? = loadData("tags_map", activity)
|
||||
suspend fun getGenresAndTags(): Boolean {
|
||||
var genres: ArrayList<String>? = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||
.toMutableList() as ArrayList<String>?
|
||||
val adultTags = PrefManager.getVal<Set<String>>(PrefName.TagsListIsAdult).toMutableList()
|
||||
val nonAdultTags =
|
||||
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
|
||||
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
|
||||
mapOf(
|
||||
true to adultTags,
|
||||
false to nonAdultTags
|
||||
)
|
||||
|
||||
if (genres == null) {
|
||||
if (genres.isNullOrEmpty()) {
|
||||
executeQuery<Query.GenreCollection>(
|
||||
"""{GenreCollection}""",
|
||||
force = true,
|
||||
@@ -424,7 +675,7 @@ class AnilistQueries {
|
||||
forEach {
|
||||
genres?.add(it)
|
||||
}
|
||||
saveData("genres_list", genres!!)
|
||||
PrefManager.setVal(PrefName.GenresList, genres?.toSet())
|
||||
}
|
||||
}
|
||||
if (tags == null) {
|
||||
@@ -442,10 +693,11 @@ class AnilistQueries {
|
||||
true to adult,
|
||||
false to good
|
||||
)
|
||||
saveData("tags_map", tags)
|
||||
PrefManager.setVal(PrefName.TagsListIsAdult, adult.toSet())
|
||||
PrefManager.setVal(PrefName.TagsListNonAdult, good.toSet())
|
||||
}
|
||||
}
|
||||
return if (genres != null && tags != null) {
|
||||
return if (!genres.isNullOrEmpty() && tags != null) {
|
||||
Anilist.genres = genres
|
||||
Anilist.tags = tags
|
||||
true
|
||||
@@ -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? {
|
||||
val genres = loadData<MutableMap<String, Genre>>("genre_thumb") ?: mutableMapOf()
|
||||
val genres: MutableMap<String, Genre> =
|
||||
loadSerializableMap<String, Genre>("genre_thumb")?.toMutableMap()
|
||||
?: mutableMapOf()
|
||||
if (genres.checkGenreTime(genre)) {
|
||||
try {
|
||||
val genreQuery =
|
||||
@@ -476,7 +757,7 @@ class AnilistQueries {
|
||||
it.bannerImage!!,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
saveData("genre_thumb", genres)
|
||||
saveSerializableMap("genre_thumb", genres)
|
||||
return genres[genre]
|
||||
}
|
||||
}
|
||||
@@ -559,18 +840,36 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||
${if (season != null) ""","season":"$season"""" 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 (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${
|
||||
if (excludedGenres?.isNotEmpty() == true)
|
||||
""","excludedGenres":[${excludedGenres.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
||||
""","excludedGenres":[${
|
||||
excludedGenres.joinToString {
|
||||
"\"${
|
||||
it.replace(
|
||||
"Not ",
|
||||
""
|
||||
)
|
||||
}\""
|
||||
}
|
||||
}]"""
|
||||
else ""
|
||||
}
|
||||
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${
|
||||
if (excludedTags?.isNotEmpty() == true)
|
||||
""","excludedTags":[${excludedTags.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
||||
""","excludedTags":[${
|
||||
excludedTags.joinToString {
|
||||
"\"${
|
||||
it.replace(
|
||||
"Not ",
|
||||
""
|
||||
)
|
||||
}\""
|
||||
}
|
||||
}]"""
|
||||
else ""
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
@@ -613,7 +912,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
page = pageInfo.currentPage.toString().toIntOrNull() ?: 0,
|
||||
hasNextPage = pageInfo.hasNextPage == true,
|
||||
)
|
||||
} else snackString(currContext()?.getString(R.string.empty_response))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -622,7 +921,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
greater: Long = 0,
|
||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||
): MutableList<Media>? {
|
||||
suspend fun execute(page:Int = 1):Page?{
|
||||
suspend fun execute(page: Int = 1): Page? {
|
||||
val query = """{
|
||||
Page(page:$page,perPage:50) {
|
||||
pageInfo {
|
||||
@@ -668,10 +967,10 @@ Page(page:$page,perPage:50) {
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||
}
|
||||
if(smaller) {
|
||||
if (smaller) {
|
||||
val response = execute()?.airingSchedules ?: return null
|
||||
val idArr = mutableListOf<Int>()
|
||||
val listOnly = loadData("recently_list_only") ?: false
|
||||
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||
return response.mapNotNull { i ->
|
||||
i.media?.let {
|
||||
if (!idArr.contains(it.id))
|
||||
@@ -682,11 +981,11 @@ Page(page:$page,perPage:50) {
|
||||
else null
|
||||
}
|
||||
}.toMutableList()
|
||||
}else{
|
||||
} else {
|
||||
var i = 1
|
||||
val list = mutableListOf<Media>()
|
||||
var res : Page? = null
|
||||
suspend fun next(){
|
||||
var res: Page? = null
|
||||
suspend fun next() {
|
||||
res = execute(i)
|
||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
||||
j.media?.let {
|
||||
@@ -694,10 +993,10 @@ Page(page:$page,perPage:50) {
|
||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
||||
} else null
|
||||
}
|
||||
}?: listOf())
|
||||
} ?: listOf())
|
||||
}
|
||||
next()
|
||||
while (res?.pageInfo?.hasNextPage == true){
|
||||
while (res?.pageInfo?.hasNextPage == true) {
|
||||
next()
|
||||
i++
|
||||
}
|
||||
@@ -822,19 +1121,20 @@ Page(page:$page,perPage:50) {
|
||||
var page = 0
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
hasNextPage = executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
val year = startDate?.year?.toString() ?: "TBA"
|
||||
val title = if (status != "CANCELLED") year else status
|
||||
if (!yearMedia.containsKey(title))
|
||||
yearMedia[title] = arrayListOf()
|
||||
yearMedia[title]?.add(Media(this))
|
||||
hasNextPage =
|
||||
executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
val year = startDate?.year?.toString() ?: "TBA"
|
||||
val title = if (status != "CANCELLED") year else status
|
||||
if (!yearMedia.containsKey(title))
|
||||
yearMedia[title] = arrayListOf()
|
||||
yearMedia[title]?.add(Media(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
it.pageInfo?.hasNextPage == true
|
||||
} ?: false
|
||||
it.pageInfo?.hasNextPage == true
|
||||
} ?: false
|
||||
}
|
||||
if (yearMedia.contains("CANCELLED")) {
|
||||
val a = yearMedia["CANCELLED"]!!
|
||||
@@ -896,7 +1196,10 @@ Page(page:$page,perPage:50) {
|
||||
|
||||
while (hasNextPage) {
|
||||
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 ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
|
||||
@@ -5,12 +5,14 @@ import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -19,9 +21,12 @@ import kotlinx.coroutines.launch
|
||||
|
||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (Discord.userid == null && Discord.token != null) {
|
||||
if (!Discord.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_discord_user_data))
|
||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||
val userid = PrefManager.getVal(PrefName.DiscordId, null as String?)
|
||||
if (userid == null && token != null) {
|
||||
/*if (!Discord.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_discord_user_data))*/
|
||||
//TODO: Discord.getUserData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,48 +43,79 @@ suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
}
|
||||
} else true
|
||||
|
||||
if(anilist) block.invoke()
|
||||
if (anilist) block.invoke()
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
||||
|
||||
private val animePlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||
suspend fun setAnimePlanned() = animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
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
|
||||
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
|
||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
||||
|
||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||
suspend fun setMangaPlanned() = mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
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
|
||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||
|
||||
suspend fun initHomePage() {
|
||||
val res = Anilist.query.initHomePage()
|
||||
res["currentAnime"]?.let { animeContinue.postValue(it) }
|
||||
res["favoriteAnime"]?.let { animeFav.postValue(it) }
|
||||
res["plannedAnime"]?.let { animePlanned.postValue(it) }
|
||||
res["currentManga"]?.let { mangaContinue.postValue(it) }
|
||||
res["favoriteManga"]?.let { mangaFav.postValue(it) }
|
||||
res["plannedManga"]?.let { mangaPlanned.postValue(it) }
|
||||
res["recommendations"]?.let { recommendation.postValue(it) }
|
||||
}
|
||||
|
||||
suspend fun loadMain(context: FragmentActivity) {
|
||||
Anilist.getSavedToken(context)
|
||||
Anilist.getSavedToken()
|
||||
MAL.getSavedToken(context)
|
||||
Discord.getSavedToken(context)
|
||||
if (loadData<Boolean>("check_update") != false) AppUpdater.check(context)
|
||||
genres.postValue(Anilist.query.getGenresAndTags(context))
|
||||
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||
}
|
||||
genres.postValue(Anilist.query.getGenresAndTags())
|
||||
}
|
||||
|
||||
val empty = MutableLiveData<Boolean>(null)
|
||||
@@ -93,7 +129,9 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
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
|
||||
suspend fun loadTrending(i: Int) {
|
||||
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
|
||||
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
||||
|
||||
@@ -157,15 +197,33 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
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
|
||||
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
|
||||
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)
|
||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
val data: Uri? = intent?.data
|
||||
logger(data.toString())
|
||||
try {
|
||||
Anilist.token = Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||
val filename = "anilistToken"
|
||||
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
|
||||
it.write(Anilist.token!!.toByteArray())
|
||||
}
|
||||
Anilist.token =
|
||||
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||
PrefManager.setVal(PrefName.AnilistToken, Anilist.token ?: "")
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,15 @@ data class SearchResults(
|
||||
val list = mutableListOf<SearchChip>()
|
||||
sort?.let {
|
||||
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 {
|
||||
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
||||
@@ -42,27 +50,37 @@ data class SearchResults(
|
||||
list.add(SearchChip("GENRE", it))
|
||||
}
|
||||
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 {
|
||||
list.add(SearchChip("TAG", it))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
fun removeChip(chip: SearchChip) {
|
||||
when (chip.type) {
|
||||
"SORT" -> sort = null
|
||||
"FORMAT" -> format = null
|
||||
"SEASON" -> season = null
|
||||
"SEASON_YEAR" -> seasonYear = null
|
||||
"GENRE" -> genres?.remove(chip.text)
|
||||
"SORT" -> sort = null
|
||||
"FORMAT" -> format = null
|
||||
"SEASON" -> season = null
|
||||
"SEASON_YEAR" -> seasonYear = null
|
||||
"GENRE" -> genres?.remove(chip.text)
|
||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||
"TAG" -> tags?.remove(chip.text)
|
||||
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
||||
"TAG" -> tags?.remove(chip.text)
|
||||
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import ani.dantotsu.themes.ThemeManager
|
||||
class UrlMedia : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||
var isMAL = false
|
||||
@@ -21,6 +22,9 @@ class UrlMedia : Activity() {
|
||||
isMAL = data?.host != "anilist.co"
|
||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||
} 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.Serializable
|
||||
|
||||
class Query{
|
||||
class Query {
|
||||
@Serializable
|
||||
data class Viewer(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Viewer")
|
||||
val user: ani.dantotsu.connections.anilist.api.User?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Media(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Media")
|
||||
@@ -30,12 +31,12 @@ class Query{
|
||||
@Serializable
|
||||
data class Page(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Page")
|
||||
val page : ani.dantotsu.connections.anilist.api.Page?
|
||||
val page: ani.dantotsu.connections.anilist.api.Page?
|
||||
)
|
||||
}
|
||||
// data class AiringSchedule(
|
||||
@@ -49,8 +50,8 @@ class Query{
|
||||
@Serializable
|
||||
data class Character(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Data(
|
||||
@@ -63,7 +64,7 @@ class Query{
|
||||
data class Studio(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Studio")
|
||||
@@ -76,7 +77,7 @@ class Query{
|
||||
data class Author(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Staff")
|
||||
@@ -95,8 +96,8 @@ class Query{
|
||||
@Serializable
|
||||
data class MediaListCollection(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@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
|
||||
data class GenreCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("GenreCollection")
|
||||
@@ -120,7 +155,7 @@ class Query{
|
||||
data class MediaTagCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("MediaTagCollection")
|
||||
@@ -132,7 +167,7 @@ class Query{
|
||||
data class User(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
|
||||
@@ -3,7 +3,7 @@ package ani.dantotsu.connections.anilist.api
|
||||
import kotlinx.serialization.SerialName
|
||||
import java.io.Serializable
|
||||
import java.text.DateFormatSymbols
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class FuzzyDate(
|
||||
@@ -16,9 +16,11 @@ data class FuzzyDate(
|
||||
fun isEmpty(): Boolean {
|
||||
return year == null && month == null && day == null
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return if ( isEmpty() ) "??" else toStringOrEmpty()
|
||||
return if (isEmpty()) "??" else toStringOrEmpty()
|
||||
}
|
||||
|
||||
fun toStringOrEmpty(): String {
|
||||
return listOfNotNull(
|
||||
day?.toString(),
|
||||
@@ -29,16 +31,21 @@ data class FuzzyDate(
|
||||
|
||||
fun getToday(): FuzzyDate {
|
||||
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 {
|
||||
return listOfNotNull(
|
||||
year?.let {"year:$it"},
|
||||
month?.let {"month:$it"},
|
||||
day?.let {"day:$it"}
|
||||
year?.let { "year:$it" },
|
||||
month?.let { "month:$it" },
|
||||
day?.let { "day:$it" }
|
||||
).joinToString(",", "{", "}")
|
||||
}
|
||||
|
||||
fun toMALString(): String {
|
||||
val padding = '0'
|
||||
val values = listOf(
|
||||
@@ -46,7 +53,7 @@ data class FuzzyDate(
|
||||
month?.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 {
|
||||
@@ -54,8 +61,8 @@ data class FuzzyDate(
|
||||
// }
|
||||
|
||||
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)
|
||||
else -> (day ?: 0) - (other.day ?: 0)
|
||||
else -> (day ?: 0) - (other.day ?: 0)
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ data class Media(
|
||||
@SerialName("characters") var characters: CharacterConnection?,
|
||||
|
||||
// The staff who produced the media
|
||||
@SerialName("staffPreview") var staff: StaffConnection?,
|
||||
@SerialName("staffPreview") var staff: StaffConnection?,
|
||||
|
||||
// The companies who produced the media
|
||||
@SerialName("studios") var studios: StudioConnection?,
|
||||
@@ -292,7 +292,7 @@ data class MediaList(
|
||||
@SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?,
|
||||
|
||||
// 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
|
||||
// @SerialName("advancedScores") var advancedScores: Json?,
|
||||
@@ -355,7 +355,7 @@ data class MediaTrailer(
|
||||
|
||||
@Serializable
|
||||
data class MediaTagCollection(
|
||||
@SerialName("tags") var tags : List<MediaTag>?
|
||||
@SerialName("tags") var tags: List<MediaTag>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -2,6 +2,7 @@ package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Recommendation(
|
||||
// The id of the recommendation
|
||||
@@ -22,6 +23,7 @@ data class Recommendation(
|
||||
// The user that first created the recommendation
|
||||
@SerialName("user") var user: User?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RecommendationConnection(
|
||||
//@SerialName("edges") var edges: List<RecommendationEdge>?,
|
||||
|
||||
@@ -9,7 +9,7 @@ data class Staff(
|
||||
@SerialName("id") var id: Int,
|
||||
|
||||
// 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
|
||||
@SerialName("languageV2") var languageV2: String?,
|
||||
@@ -80,8 +80,8 @@ data class Staff(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StaffName (
|
||||
var userPreferred:String?
|
||||
data class StaffName(
|
||||
var userPreferred: String?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -96,6 +96,6 @@ data class StaffConnection(
|
||||
|
||||
@Serializable
|
||||
data class StaffEdge(
|
||||
var role:String?,
|
||||
var role: String?,
|
||||
var node: Staff?
|
||||
)
|
||||
@@ -80,10 +80,10 @@ data class UserOptions(
|
||||
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
|
||||
|
||||
// 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)
|
||||
@SerialName("profileColor") var profileColor: String?,
|
||||
// Profile highlight color (blue, purple, pink, orange, red, green, gray)
|
||||
@SerialName("profileColor") var profileColor: String?,
|
||||
//
|
||||
// // Notification options
|
||||
// // @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.Intent
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.edit
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.serializers.User
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWith
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.io.File
|
||||
|
||||
object Discord {
|
||||
@@ -21,37 +19,20 @@ object Discord {
|
||||
var userid: String? = null
|
||||
var avatar: String? = null
|
||||
|
||||
private const val TOKEN = "discord_token"
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
token = PrefManager.getVal(
|
||||
PrefName.DiscordToken, null as String?
|
||||
)
|
||||
token = sharedPref.getString(TOKEN, null)
|
||||
return token != null
|
||||
}
|
||||
|
||||
fun saveToken(context: Context, token: String) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPref.edit {
|
||||
putString(TOKEN, token)
|
||||
commit()
|
||||
}
|
||||
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPref.edit {
|
||||
remove(TOKEN)
|
||||
commit()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.DiscordToken)
|
||||
|
||||
tryWith(true) {
|
||||
val dir = File(context.filesDir?.parentFile, "app_webview")
|
||||
@@ -60,17 +41,7 @@ object Discord {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
private var rpc: RPC? = null
|
||||
|
||||
|
||||
fun warning(context: Context) = CustomBottomDialog().apply {
|
||||
@@ -97,16 +68,21 @@ object Discord {
|
||||
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 {
|
||||
RPC(it, Dispatchers.IO).apply {
|
||||
applicationId = "1163925779692912771"
|
||||
applicationId = application_Id
|
||||
smallImage = RPC.Link(
|
||||
"Dantotsu",
|
||||
"mp:attachments/1163940221063278672/1163940262423298141/bitmap1024.png"
|
||||
small_Image
|
||||
)
|
||||
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,9 +4,10 @@ import android.annotation.SuppressLint
|
||||
import android.app.Application.getProcessName
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||
@@ -18,6 +19,7 @@ class Login : AppCompatActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = getProcessName()
|
||||
@@ -32,28 +34,46 @@ class Login : AppCompatActivity() {
|
||||
settings.databaseEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
}
|
||||
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if (url != null && url.endsWith("/app")) {
|
||||
webView.stopLoading()
|
||||
webView.evaluateJavascript("""
|
||||
(function() {
|
||||
const wreq = webpackChunkdiscord_app.push([[Symbol()], {}, w => w])
|
||||
webpackChunkdiscord_app.pop()
|
||||
const token = Object.values(wreq.c).find(m => m.exports?.Z?.getToken).exports.Z.getToken();
|
||||
return token;
|
||||
})()
|
||||
""".trimIndent()){
|
||||
login(it.trim('"'))
|
||||
}
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
// Check if the URL is the one expected after a successful login
|
||||
if (request?.url.toString() != "https://discord.com/login") {
|
||||
// Delay the script execution to ensure the page is fully loaded
|
||||
view?.postDelayed({
|
||||
view.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
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")
|
||||
}
|
||||
|
||||
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()
|
||||
saveToken(this, token)
|
||||
startMainActivity(this@Login)
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
package ani.dantotsu.connections.discord
|
||||
|
||||
import ani.dantotsu.connections.discord.serializers.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import ani.dantotsu.connections.discord.serializers.Activity
|
||||
import ani.dantotsu.connections.discord.serializers.Presence
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
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 ani.dantotsu.client as app
|
||||
|
||||
@@ -31,205 +17,73 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
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 {
|
||||
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
|
||||
}
|
||||
|
||||
var buttons = mutableListOf<Link>()
|
||||
|
||||
data class Link(val label: String, val url: String)
|
||||
|
||||
private suspend fun createPresence(): String {
|
||||
return json.encodeToString(Presence.Response(
|
||||
3,
|
||||
Presence(
|
||||
activities = listOf(
|
||||
Activity(
|
||||
name = activityName,
|
||||
state = state,
|
||||
details = details,
|
||||
type = type?.ordinal,
|
||||
timestamps = if (startTimestamp != null)
|
||||
Activity.Timestamps(startTimestamp, stopTimestamp)
|
||||
else null,
|
||||
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
|
||||
)
|
||||
companion object {
|
||||
data class RPCData(
|
||||
val applicationId: String? = null,
|
||||
val type: Type? = null,
|
||||
val activityName: String? = null,
|
||||
val details: String? = null,
|
||||
val state: String? = null,
|
||||
val largeImage: Link? = null,
|
||||
val smallImage: Link? = null,
|
||||
val status: String? = null,
|
||||
val startTimestamp: Long? = null,
|
||||
val stopTimestamp: Long? = null,
|
||||
val buttons: MutableList<Link> = mutableListOf()
|
||||
)
|
||||
webSocket.send(json.encodeToString(response))
|
||||
}
|
||||
|
||||
fun send(block: RPC.() -> Unit) {
|
||||
block.invoke(this)
|
||||
send()
|
||||
}
|
||||
@Serializable
|
||||
data class KizzyApi(val id: String)
|
||||
|
||||
var started = false
|
||||
var whenStarted: ((User) -> Unit)? = null
|
||||
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
|
||||
}
|
||||
|
||||
fun send() {
|
||||
val send = {
|
||||
CoroutineScope(coroutineContext).launch {
|
||||
webSocket.send(createPresence())
|
||||
suspend fun createPresence(data: RPCData): String {
|
||||
val json = Json {
|
||||
encodeDefaults = true
|
||||
allowStructuredMapKeys = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
}
|
||||
if (!started) whenStarted = {
|
||||
send.invoke()
|
||||
whenStarted = null
|
||||
}
|
||||
else send.invoke()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
webSocket.send(
|
||||
json.encodeToString(
|
||||
Presence.Response(
|
||||
3,
|
||||
Presence(status = "offline")
|
||||
return json.encodeToString(Presence.Response(
|
||||
3,
|
||||
Presence(
|
||||
activities = listOf(
|
||||
Activity(
|
||||
name = data.activityName,
|
||||
state = data.state,
|
||||
details = data.details,
|
||||
type = data.type?.ordinal,
|
||||
timestamps = if (data.startTimestamp != null)
|
||||
Activity.Timestamps(data.startTimestamp, data.stopTimestamp)
|
||||
else null,
|
||||
assets = Activity.Assets(
|
||||
largeImage = data.largeImage?.url?.discordUrl(),
|
||||
largeText = data.largeImage?.label,
|
||||
smallImage = data.smallImage?.url?.discordUrl(),
|
||||
smallText = data.smallImage?.label
|
||||
),
|
||||
buttons = data.buttons.map { it.label },
|
||||
metadata = Activity.Metadata(
|
||||
buttonUrls = data.buttons.map { it.url }
|
||||
),
|
||||
applicationId = data.applicationId,
|
||||
)
|
||||
),
|
||||
afk = true,
|
||||
since = data.startTimestamp,
|
||||
status = data.status
|
||||
)
|
||||
)
|
||||
)
|
||||
webSocket.close(4000, "Interrupt")
|
||||
}
|
||||
|
||||
//I hate this, but couldn't find any better way to solve it
|
||||
suspend fun getUserData(): User {
|
||||
var user : User? = null
|
||||
whenStarted = {
|
||||
user = it
|
||||
whenStarted = null
|
||||
}
|
||||
while (user == null) {
|
||||
delay(100)
|
||||
}
|
||||
return user!!
|
||||
}
|
||||
|
||||
var onReceiveUserData: ((User) -> Deferred<Unit>)? = null
|
||||
|
||||
inner class Listener : WebSocketListener() {
|
||||
private var seq: Int? = null
|
||||
private var heartbeatInterval: Long? = null
|
||||
|
||||
var scope = CoroutineScope(coroutineContext)
|
||||
|
||||
private fun sendHeartBeat() {
|
||||
scope.cancel()
|
||||
scope = CoroutineScope(coroutineContext)
|
||||
scope.launch {
|
||||
delay(heartbeatInterval!!)
|
||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
println("Message : $text")
|
||||
|
||||
val map = json.decodeFromString<Res>(text)
|
||||
seq = map.s
|
||||
|
||||
when (map.op) {
|
||||
10 -> {
|
||||
map.d as JsonObject
|
||||
heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long
|
||||
sendHeartBeat()
|
||||
sendIdentify()
|
||||
}
|
||||
|
||||
0 -> if (map.t == "READY") {
|
||||
val user = json.decodeFromString<User.Response>(text).d.user
|
||||
started = true
|
||||
whenStarted?.invoke(user)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
if (scope.isActive) scope.cancel()
|
||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
||||
}
|
||||
|
||||
11 -> sendHeartBeat()
|
||||
7 -> webSocket.close(400, "Reconnect")
|
||||
9 -> {
|
||||
sendHeartBeat()
|
||||
sendIdentify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
println("Server Closed : $code $reason")
|
||||
if (code == 4000) {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
println("Failure : ${t.message}")
|
||||
if (t.message != "Interrupt") {
|
||||
this@RPC.webSocket = client.newWebSocket(request, Listener())
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Activity (
|
||||
data class Activity(
|
||||
@SerialName("application_id")
|
||||
val applicationId: String? = null,
|
||||
val name: String? = null,
|
||||
|
||||
@@ -12,13 +12,13 @@ data class Identity(
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Response (
|
||||
data class Response(
|
||||
val op: Long,
|
||||
val d: Identity
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Properties (
|
||||
data class Properties(
|
||||
@SerialName("\$os")
|
||||
val os: String,
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ package ani.dantotsu.connections.discord.serializers
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Presence (
|
||||
data class Presence(
|
||||
val activities: List<Activity> = listOf(),
|
||||
val afk: Boolean = true,
|
||||
val since: Long? = null,
|
||||
val status: String? = null
|
||||
){
|
||||
) {
|
||||
@Serializable
|
||||
data class Response (
|
||||
data class Response(
|
||||
val op: Long,
|
||||
val d: Presence
|
||||
)
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
|
||||
@Serializable
|
||||
data class User (
|
||||
val verified: Boolean,
|
||||
data class User(
|
||||
val verified: Boolean? = null,
|
||||
val username: String,
|
||||
|
||||
@SerialName("purchased_flags")
|
||||
val purchasedFlags: Long,
|
||||
val purchasedFlags: Long? = null,
|
||||
|
||||
@SerialName("public_flags")
|
||||
val publicFlags: Long,
|
||||
val publicFlags: Long? = null,
|
||||
|
||||
val pronouns: String,
|
||||
val pronouns: String? = null,
|
||||
|
||||
@SerialName("premium_type")
|
||||
val premiumType: Long,
|
||||
val premiumType: Long? = null,
|
||||
|
||||
val premium: Boolean,
|
||||
val phone: String,
|
||||
val premium: Boolean? = null,
|
||||
val phone: String? = null,
|
||||
|
||||
@SerialName("nsfw_allowed")
|
||||
val nsfwAllowed: Boolean,
|
||||
val nsfwAllowed: Boolean? = null,
|
||||
|
||||
val mobile: Boolean,
|
||||
val mobile: Boolean? = null,
|
||||
|
||||
@SerialName("mfa_enabled")
|
||||
val mfaEnabled: Boolean,
|
||||
val mfaEnabled: Boolean? = null,
|
||||
|
||||
val id: String,
|
||||
|
||||
@SerialName("global_name")
|
||||
val globalName: String,
|
||||
val globalName: String? = null,
|
||||
|
||||
val flags: Long,
|
||||
val email: String,
|
||||
val discriminator: String,
|
||||
val desktop: Boolean,
|
||||
val bio: String,
|
||||
val flags: Long? = null,
|
||||
val email: String? = null,
|
||||
val discriminator: String? = null,
|
||||
val desktop: Boolean? = null,
|
||||
val bio: String? = null,
|
||||
|
||||
@SerialName("banner_color")
|
||||
val bannerColor: String,
|
||||
val bannerColor: String? = null,
|
||||
|
||||
val banner: JsonElement? = null,
|
||||
|
||||
@SerialName("avatar_decoration")
|
||||
val avatarDecoration: JsonElement? = null,
|
||||
|
||||
val avatar: String,
|
||||
val avatar: String? = null,
|
||||
|
||||
@SerialName("accent_color")
|
||||
val accentColor: Long
|
||||
val accentColor: Long? = null
|
||||
) {
|
||||
@Serializable
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,29 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.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.tryWithSuspend
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
try {
|
||||
val data: Uri = intent?.data
|
||||
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
||||
val codeChallenge: String = loadData("malCodeChallenge", this)
|
||||
val codeChallenge = PrefManager.getVal(PrefName.MALCodeChallenge, null as String?)
|
||||
?: throw Exception(getString(R.string.mal_login_code_challenge_not_found))
|
||||
val code = data.getQueryParameter("code")
|
||||
?: throw Exception(getString(R.string.mal_login_code_not_present))
|
||||
@@ -44,9 +52,8 @@ class Login : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e:Exception){
|
||||
logError(e,snackbar = false)
|
||||
} catch (e: Exception) {
|
||||
logError(e, snackbar = false)
|
||||
startMainActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,15 @@ import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
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.Serializable
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
object MAL {
|
||||
@@ -28,7 +33,7 @@ object MAL {
|
||||
.replace("/", "_")
|
||||
.replace("\n", "")
|
||||
|
||||
saveData("malCodeChallenge", codeChallenge, context)
|
||||
PrefManager.setVal(PrefName.MALCodeChallenge, codeChallenge)
|
||||
val request =
|
||||
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
|
||||
try {
|
||||
@@ -41,11 +46,9 @@ object MAL {
|
||||
}
|
||||
}
|
||||
|
||||
private const val MAL_TOKEN = "malToken"
|
||||
|
||||
private suspend fun refreshToken(): ResponseToken? {
|
||||
return tryWithSuspend {
|
||||
val token = loadData<ResponseToken>(MAL_TOKEN)
|
||||
val token = PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
|
||||
val res = client.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
@@ -63,8 +66,9 @@ object MAL {
|
||||
|
||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
||||
return tryWithSuspend(false) {
|
||||
var res: ResponseToken = loadData(MAL_TOKEN, context)
|
||||
?: return@tryWithSuspend false
|
||||
var res: ResponseToken =
|
||||
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||
?: return@tryWithSuspend false
|
||||
if (System.currentTimeMillis() > res.expiresIn)
|
||||
res = refreshToken()
|
||||
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
|
||||
@@ -78,14 +82,12 @@ object MAL {
|
||||
username = null
|
||||
userid = null
|
||||
avatar = null
|
||||
if (MAL_TOKEN in context.fileList()) {
|
||||
File(context.filesDir, MAL_TOKEN).delete()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.MALToken)
|
||||
}
|
||||
|
||||
fun saveResponse(res: ResponseToken) {
|
||||
res.expiresIn += System.currentTimeMillis()
|
||||
saveData(MAL_TOKEN, res)
|
||||
PrefManager.setVal(PrefName.MALToken, res)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -94,6 +96,10 @@ object MAL {
|
||||
@SerialName("expires_in") var expiresIn: Long,
|
||||
@SerialName("access_token") val accessToken: String,
|
||||
@SerialName("refresh_token") val refreshToken: String,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID = 1L
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ani.dantotsu.connections.mal
|
||||
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -43,18 +43,18 @@ class MALQueries {
|
||||
start: FuzzyDate? = null,
|
||||
end: FuzzyDate? = null
|
||||
) {
|
||||
if(idMAL==null) return
|
||||
if (idMAL == null) return
|
||||
val data = mutableMapOf("status" to convertStatus(isAnime, status))
|
||||
if (progress != null)
|
||||
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
|
||||
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
|
||||
if (score != null)
|
||||
data["score"] = score.div(10).toString()
|
||||
if(rewatch!=null)
|
||||
data[if(isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
||||
if(start!=null)
|
||||
if (rewatch != null)
|
||||
data[if (isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
||||
if (start != null)
|
||||
data["start_date"] = start.toMALString()
|
||||
if(end!=null)
|
||||
if (end != null)
|
||||
data["finish_date"] = end.toMALString()
|
||||
tryWithSuspend {
|
||||
client.put(
|
||||
@@ -65,8 +65,8 @@ class MALQueries {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteList(isAnime: Boolean, idMAL: Int?){
|
||||
if(idMAL==null) return
|
||||
suspend fun deleteList(isAnime: Boolean, idMAL: Int?) {
|
||||
if (idMAL == null) return
|
||||
tryWithSuspend {
|
||||
client.delete(
|
||||
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
|
||||
|
||||
310
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
310
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
@@ -0,0 +1,310 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
|
||||
class DownloadsManager(private val context: Context) {
|
||||
private val gson = Gson()
|
||||
private val downloadsList = loadDownloads().toMutableList()
|
||||
|
||||
val mangaDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
||||
val animeDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
||||
val novelDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
||||
|
||||
private fun saveDownloads() {
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
PrefManager.setVal(PrefName.DownloadsKeys, jsonString)
|
||||
}
|
||||
|
||||
private fun loadDownloads(): List<DownloadedType> {
|
||||
val jsonString = PrefManager.getVal(PrefName.DownloadsKeys, null as String?)
|
||||
return if (jsonString != null) {
|
||||
val type = object : TypeToken<List<DownloadedType>>() {}.type
|
||||
gson.fromJson(jsonString, type)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addDownload(downloadedType: DownloadedType) {
|
||||
downloadsList.add(downloadedType)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(downloadedType: DownloadedType) {
|
||||
downloadsList.remove(downloadedType)
|
||||
removeDirectory(downloadedType)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory/$title"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
cleanDownloads()
|
||||
}
|
||||
when (type) {
|
||||
DownloadedType.Type.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
||||
}
|
||||
|
||||
DownloadedType.Type.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
||||
}
|
||||
|
||||
DownloadedType.Type.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
private fun cleanDownloads() {
|
||||
cleanDownload(DownloadedType.Type.MANGA)
|
||||
cleanDownload(DownloadedType.Type.ANIME)
|
||||
cleanDownload(DownloadedType.Type.NOVEL)
|
||||
}
|
||||
|
||||
private fun cleanDownload(type: DownloadedType.Type) {
|
||||
// remove all folders that are not in the downloads list
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory"
|
||||
)
|
||||
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
||||
mangaDownloadedTypes
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
animeDownloadedTypes
|
||||
} else {
|
||||
novelDownloadedTypes
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val files = directory.listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
val deleted = file.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//now remove all downloads that do not have a folder
|
||||
val iterator = downloadsList.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val download = iterator.next()
|
||||
val downloadDir = File(directory, download.title)
|
||||
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||
{
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
val file = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/downloads.json"
|
||||
)
|
||||
if (file.parentFile?.exists() == false) {
|
||||
file.parentFile?.mkdirs()
|
||||
}
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
|
||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||
return downloadsList.contains(downloadedType)
|
||||
}
|
||||
|
||||
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
||||
return if (type == null) {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||
} else {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
val destination = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val copied = directory.copyRecursively(destination, true)
|
||||
if (copied) {
|
||||
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: DownloadedType.Type) {
|
||||
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
downloadsList.removeAll { it.type == type }
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val novelLocation = "Dantotsu/Novel"
|
||||
const val mangaLocation = "Dantotsu/Manga"
|
||||
const val animeLocation = "Dantotsu/Anime"
|
||||
|
||||
fun getDirectory(
|
||||
context: Context,
|
||||
type: DownloadedType.Type,
|
||||
title: String,
|
||||
chapter: String? = null
|
||||
): File {
|
||||
return if (type == DownloadedType.Type.MANGA) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title"
|
||||
)
|
||||
}
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME,
|
||||
NOVEL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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?,
|
||||
)
|
||||
@@ -0,0 +1,425 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.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.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class MangaDownloaderService : Service() {
|
||||
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
private lateinit var builder: NotificationCompat.Builder
|
||||
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
private val downloadJobs = mutableMapOf<String, Job>()
|
||||
private val mutex = Mutex()
|
||||
private var isCurrentlyProcessing = false
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Manga Download Progress")
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
cancelReceiver,
|
||||
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
MangaServiceDataSingleton.downloadQueue.clear()
|
||||
downloadJobs.clear()
|
||||
MangaServiceDataSingleton.isServiceRunning = false
|
||||
unregisterReceiver(cancelReceiver)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
snackString("Download started")
|
||||
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
serviceScope.launch {
|
||||
mutex.withLock {
|
||||
if (!isCurrentlyProcessing) {
|
||||
isCurrentlyProcessing = true
|
||||
processQueue()
|
||||
isCurrentlyProcessing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun processQueue() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
while (MangaServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||
val task = MangaServiceDataSingleton.downloadQueue.poll()
|
||||
if (task != null) {
|
||||
val job = launch { download(task) }
|
||||
mutex.withLock {
|
||||
downloadJobs[task.chapter] = job
|
||||
}
|
||||
job.join() // Wait for the job to complete before continuing to the next task
|
||||
mutex.withLock {
|
||||
downloadJobs.remove(task.chapter)
|
||||
}
|
||||
updateNotification() // Update the notification after each task is completed
|
||||
}
|
||||
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
stopSelf() // Stop the service when the queue is empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelDownload(chapter: String) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
mutex.withLock {
|
||||
downloadJobs[chapter]?.cancel()
|
||||
downloadJobs.remove(chapter)
|
||||
MangaServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
|
||||
updateNotification() // Update the notification after cancellation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
// Update the notification to reflect the current state of the queue
|
||||
val pendingDownloads = MangaServiceDataSingleton.downloadQueue.size
|
||||
val text = if (pendingDownloads > 0) {
|
||||
"Pending downloads: $pendingDownloads"
|
||||
} else {
|
||||
"All downloads completed"
|
||||
}
|
||||
builder.setContentText(text)
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
suspend fun download(task: DownloadTask) {
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
this@MangaDownloaderService,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
if (deferredMap.size >= task.simultaneousDownloads) {
|
||||
deferredMap.values.awaitAll()
|
||||
deferredMap.clear()
|
||||
}
|
||||
|
||||
deferredMap[index] = async(Dispatchers.IO) {
|
||||
var bitmap: Bitmap? = null
|
||||
var retryCount = 0
|
||||
|
||||
while (bitmap == null && retryCount < task.retries) {
|
||||
bitmap = image.fetchAndProcessImage(
|
||||
image.page,
|
||||
image.source,
|
||||
this@MangaDownloaderService
|
||||
)
|
||||
retryCount++
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
||||
}
|
||||
farthest++
|
||||
builder.setProgress(task.imageData.size, farthest, false)
|
||||
broadcastDownloadProgress(
|
||||
task.chapter,
|
||||
farthest * 100 / task.imageData.size
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for any remaining deferred to complete
|
||||
deferredMap.values.awaitAll()
|
||||
|
||||
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||
.setProgress(0, 0, false)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
|
||||
saveMediaInfo(task)
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
DownloadedType.Type.MANGA
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.chapter)
|
||||
snackString("${task.title} - ${task.chapter} Download finished")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading file: ${e.message}")
|
||||
snackString("Exception while downloading file: ${e.message}")
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
broadcastDownloadFailed(task.chapter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||
try {
|
||||
// Define the directory within the private external storage space
|
||||
val directory = File(
|
||||
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$title/$chapter"
|
||||
)
|
||||
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Create a file reference within that directory for your image
|
||||
val file = File(directory, fileName)
|
||||
|
||||
// Use a FileOutputStream to write the bitmap to the file
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Exception while saving image: ${e.message}")
|
||||
snackString("Exception while saving image: ${e.message}")
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${task.title}"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val mediaJson = gson.toJson(task.sourceMedia)
|
||||
val media = gson.fromJson(mediaJson, Media::class.java)
|
||||
if (media != null) {
|
||||
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
|
||||
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
file.writeText(jsonString)
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@MangaDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
try {
|
||||
connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@MangaDownloaderService,
|
||||
"Exception while saving ${name}: ${e.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
null
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastDownloadStarted(chapterNumber: String) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFinished(chapterNumber: String) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_FINISHED).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFailed(chapterNumber: String) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_FAILED).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadProgress(chapterNumber: String, progress: Int) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_PROGRESS).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
putExtra("progress", progress)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private val cancelReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
|
||||
val chapter = intent.getStringExtra(EXTRA_CHAPTER)
|
||||
chapter?.let {
|
||||
cancelDownload(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class DownloadTask(
|
||||
val title: String,
|
||||
val chapter: String,
|
||||
val imageData: List<ImageData>,
|
||||
val sourceMedia: Media? = null,
|
||||
val retries: Int = 2,
|
||||
val simultaneousDownloads: Int = 2,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1103
|
||||
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
|
||||
const val EXTRA_CHAPTER = "extra_chapter"
|
||||
}
|
||||
}
|
||||
|
||||
object MangaServiceDataSingleton {
|
||||
var imageData: List<ImageData> = listOf()
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
var isServiceRunning: Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
|
||||
class OfflineMangaAdapter(
|
||||
private val context: Context,
|
||||
private var items: List<OfflineMangaModel>,
|
||||
private val searchListener: OfflineMangaSearchListener
|
||||
) : BaseAdapter() {
|
||||
private val inflater: LayoutInflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var originalItems: List<OfflineMangaModel> = items
|
||||
private var style: 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 OfflineMangaModel
|
||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
||||
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
||||
|
||||
if (style == 0) {
|
||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||
val chapters = view.findViewById<TextView>(R.id.itemTotal)
|
||||
chapters.text = " Chapters"
|
||||
bannerView.setImageURI(item.banner)
|
||||
totalChapter.text = item.totalChapter
|
||||
} else if (style == 1) {
|
||||
val readChapter =
|
||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||
readChapter.text = item.readChapter
|
||||
totalChapter.text = " | " + item.totalChapter
|
||||
}
|
||||
|
||||
// Bind item data to the views
|
||||
typeImage.setImageResource(if (item.type == "Novel") R.drawable.ic_round_book_24 else R.drawable.ic_round_import_contacts_24)
|
||||
type.text = item.type
|
||||
typeView.visibility = View.VISIBLE
|
||||
imageView.setImageURI(item.image)
|
||||
titleTextView.text = item.title
|
||||
itemScore.text = item.score
|
||||
|
||||
if (item.isOngoing) {
|
||||
ongoing.visibility = View.VISIBLE
|
||||
} else {
|
||||
ongoing.visibility = View.GONE
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
fun onSearchQuery(query: String) {
|
||||
// Implement the filtering logic here, for example:
|
||||
items = if (query.isEmpty()) {
|
||||
// Return the original list if the query is empty
|
||||
originalItems
|
||||
} else {
|
||||
// Filter the list based on the query
|
||||
originalItems.filter { it.title.contains(query, ignoreCase = true) }
|
||||
}
|
||||
notifyDataSetChanged() // Notify the adapter that the data set has changed
|
||||
}
|
||||
|
||||
fun setItems(items: List<OfflineMangaModel>) {
|
||||
this.items = items
|
||||
this.originalItems = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun notifyNewGrid() {
|
||||
style = PrefManager.getVal(PrefName.OfflineView)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
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.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.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.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 OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
private var downloads: List<OfflineMangaModel> = listOf()
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineMangaAdapter
|
||||
private lateinit var total: TextView
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
|
||||
|
||||
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
|
||||
textInputLayout.hint = "Manga"
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||
animeUserAvatar.setSafeOnClickListener {
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
if (!(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
|
||||
}
|
||||
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||
gridView.adapter = adapter
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text =
|
||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
gridView.setOnItemClickListener { _, _, position, _ ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val media =
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
media?.let {
|
||||
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
}
|
||||
|
||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: DownloadedType.Type =
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||
DownloadedType.Type.MANGA
|
||||
} else {
|
||||
DownloadedType.Type.NOVEL
|
||||
}
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
builder.setTitle("Delete ${item.title}?")
|
||||
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text =
|
||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearchQuery(query: String) {
|
||||
adapter.onSearchQuery(query)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initActivity(requireActivity())
|
||||
|
||||
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.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
}
|
||||
downloads += newNovelDownloads
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
.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 loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
DownloadedType.Type.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${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 readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val chapters = " Chapters"
|
||||
return OfflineMangaModel(
|
||||
title,
|
||||
score,
|
||||
totalchapter,
|
||||
readchapter,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
isUserScored,
|
||||
coverUri,
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OfflineMangaSearchListener {
|
||||
fun onSearchQuery(query: String)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
data class OfflineMangaModel(
|
||||
val title: String,
|
||||
val score: String,
|
||||
val totalChapter: String,
|
||||
val readChapter: String,
|
||||
val type: String,
|
||||
val chapters: String,
|
||||
val isOngoing: Boolean,
|
||||
val isUserScored: Boolean,
|
||||
val image: Uri?,
|
||||
val banner: Uri?
|
||||
)
|
||||
@@ -0,0 +1,478 @@
|
||||
package ani.dantotsu.download.novel
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.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
|
||||
|
||||
@UnstableApi
|
||||
class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) {
|
||||
class ExoplayerDownloadService :
|
||||
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||
companion object {
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||
@@ -21,10 +22,13 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
|
||||
|
||||
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(
|
||||
this,
|
||||
R.drawable.monochrome,
|
||||
R.drawable.mono,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
@@ -1,12 +1,21 @@
|
||||
package ani.dantotsu.download.video
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
@@ -15,29 +24,41 @@ import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.Requirements
|
||||
import androidx.media3.ui.TrackSelectionDialogBuilder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.*
|
||||
|
||||
object Helper {
|
||||
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun downloadVideo(context : Context, video: Video, subtitle: Subtitle?){
|
||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
@@ -49,7 +70,7 @@ object Helper {
|
||||
val mimeType = when (video.format) {
|
||||
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
||||
else -> MimeTypes.APPLICATION_MP4
|
||||
else -> MimeTypes.APPLICATION_MP4
|
||||
}
|
||||
|
||||
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
||||
@@ -76,28 +97,15 @@ object Helper {
|
||||
DefaultRenderersFactory(context),
|
||||
dataSourceFactory
|
||||
)
|
||||
downloadHelper.prepare(object : DownloadHelper.Callback{
|
||||
downloadHelper.prepare(object : DownloadHelper.Callback {
|
||||
override fun onPrepared(helper: DownloadHelper) {
|
||||
TrackSelectionDialogBuilder(context,"Select thingy",helper.getTracks(0).groups
|
||||
) { _, overrides ->
|
||||
val params = TrackSelectionParameters.Builder(context)
|
||||
overrides.forEach{
|
||||
params.addOverride(it.value)
|
||||
}
|
||||
helper.addTrackSelection(0, params.build())
|
||||
MyDownloadService
|
||||
helper.getDownloadRequest(null).let {
|
||||
DownloadService.sendAddDownload(
|
||||
context,
|
||||
MyDownloadService::class.java,
|
||||
helper.getDownloadRequest(null),
|
||||
ExoplayerDownloadService::class.java,
|
||||
it,
|
||||
false
|
||||
)
|
||||
}.apply {
|
||||
setTheme(R.style.DialogTheme)
|
||||
setTrackNameProvider {
|
||||
if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
|
||||
}
|
||||
build().show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,31 +117,61 @@ object Helper {
|
||||
|
||||
|
||||
private var download: DownloadManager? = null
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads"
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
|
||||
@Synchronized
|
||||
@UnstableApi
|
||||
fun downloadManager(context: Context): DownloadManager {
|
||||
return download ?: let {
|
||||
val database = StandaloneDatabaseProvider(context)
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val okHttpClient = networkHelper.client
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
dataSource
|
||||
}
|
||||
DownloadManager(
|
||||
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||
val downloadManager = DownloadManager(
|
||||
context,
|
||||
database,
|
||||
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database),
|
||||
getSimpleCache(context),
|
||||
dataSourceFactory,
|
||||
Executor(Runnable::run)
|
||||
executorService
|
||||
).apply {
|
||||
requirements = Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
requirements =
|
||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
maxParallelDownloads = 3
|
||||
}
|
||||
downloadManager.addListener( //for testing
|
||||
object : DownloadManager.Listener {
|
||||
override fun onDownloadChanged(
|
||||
downloadManager: DownloadManager,
|
||||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
Log.e("Downloader", "Download Completed")
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
Log.e("Downloader", "Download Failed")
|
||||
} else if (download.state == Download.STATE_STOPPED) {
|
||||
Log.e("Downloader", "Download Stopped")
|
||||
} else if (download.state == Download.STATE_QUEUED) {
|
||||
Log.e("Downloader", "Download Queued")
|
||||
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||
Log.e("Downloader", "Download Downloading")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
downloadManager
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,4 +187,102 @@ object Helper {
|
||||
}
|
||||
return downloadDirectory!!
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun startAnimeDownloadService(
|
||||
context: Context,
|
||||
title: String,
|
||||
episode: String,
|
||||
video: Video,
|
||||
subtitle: Subtitle? = null,
|
||||
sourceMedia: Media? = null,
|
||||
episodeImage: String? = null
|
||||
) {
|
||||
if (!isNotificationPermissionGranted(context)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context as Activity,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
|
||||
title,
|
||||
episode,
|
||||
video,
|
||||
subtitle,
|
||||
sourceMedia,
|
||||
episodeImage
|
||||
)
|
||||
|
||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||
val downloadCheck = downloadsManger
|
||||
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
||||
|
||||
if (downloadCheck) {
|
||||
AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Exists")
|
||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||
.setPositiveButton("Yes") { _, _ ->
|
||||
DownloadService.sendRemoveDownload(
|
||||
context,
|
||||
ExoplayerDownloadService::class.java,
|
||||
PrefManager.getAnimeDownloadPreferences().getString(
|
||||
animeDownloadTask.getTaskName(),
|
||||
""
|
||||
) ?: "",
|
||||
false
|
||||
)
|
||||
PrefManager.getAnimeDownloadPreferences().edit()
|
||||
.remove(animeDownloadTask.getTaskName())
|
||||
.apply()
|
||||
downloadsManger.removeDownload(
|
||||
DownloadedType(
|
||||
title,
|
||||
episode,
|
||||
DownloadedType.Type.ANIME
|
||||
)
|
||||
)
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
.setNegativeButton("No") { _, _ -> }
|
||||
.show()
|
||||
} else {
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun getSimpleCache(context: Context): SimpleCache {
|
||||
return if (simpleCache == null) {
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
|
||||
simpleCache!!
|
||||
} else {
|
||||
simpleCache!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -27,13 +27,13 @@ import ani.dantotsu.connections.anilist.AnilistAnimeViewModel
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.databinding.FragmentAnimeBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.ProgressAdapter
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -47,8 +47,7 @@ import kotlin.math.min
|
||||
class AnimeFragment : Fragment() {
|
||||
private var _binding: FragmentAnimeBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private lateinit var animePageAdapter: AnimePageAdapter
|
||||
|
||||
val model: AnilistAnimeViewModel by activityViewModels()
|
||||
|
||||
@@ -94,7 +93,7 @@ class AnimeFragment : Fragment() {
|
||||
|
||||
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||
|
||||
val animePageAdapter = AnimePageAdapter()
|
||||
animePageAdapter = AnimePageAdapter()
|
||||
|
||||
var loading = true
|
||||
if (model.notSet) {
|
||||
@@ -214,7 +213,7 @@ class AnimeFragment : Fragment() {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateTrending(
|
||||
MediaAdaptor(
|
||||
if (uiSettings.smallView) 3 else 2,
|
||||
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
|
||||
it,
|
||||
requireActivity(),
|
||||
viewPager = animePageAdapter.trendingViewPager
|
||||
@@ -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.loadTrending(1)
|
||||
model.loadUpdated()
|
||||
model.loadPopular("ANIME", sort = Anilist.sortBy[1])
|
||||
model.loadPopular(
|
||||
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularAnimeList
|
||||
)
|
||||
)
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.animeRefresh?.isRefreshing = false
|
||||
@@ -275,6 +279,11 @@ class AnimeFragment : Fragment() {
|
||||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||
if (animePageAdapter.trendingViewPager != null) {
|
||||
binding.root.requestApplyInsets()
|
||||
binding.root.requestLayout()
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package ani.dantotsu.home
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -15,14 +16,14 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.MediaPageTransformer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.CalendarActivity
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.px
|
||||
@@ -30,7 +31,8 @@ import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -41,10 +43,10 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
|
||||
val binding = ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return AnimePageViewHolder(binding)
|
||||
}
|
||||
|
||||
@@ -54,14 +56,21 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||
val materialCardView =
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = (-108f).px
|
||||
}
|
||||
|
||||
@@ -81,7 +90,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
binding.animeUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
listOf(
|
||||
@@ -111,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 ->
|
||||
onIncludeListClick.invoke(isChecked)
|
||||
|
||||
PrefManager.setVal(PrefName.PopularAnimeList, isChecked)
|
||||
}
|
||||
if (ready.value == false)
|
||||
ready.postValue(true)
|
||||
}
|
||||
|
||||
lateinit var onSeasonClick : ((Int)->Unit)
|
||||
lateinit var onSeasonLongClick : ((Int)->Boolean)
|
||||
lateinit var onIncludeListClick : ((Boolean)->Unit)
|
||||
lateinit var onSeasonClick: ((Int) -> Unit)
|
||||
lateinit var onSeasonLongClick: ((Int) -> Boolean)
|
||||
lateinit var onIncludeListClick: ((Boolean) -> Unit)
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
@@ -138,7 +155,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.animeTrendingViewPager.currentItem = binding.animeTrendingViewPager.currentItem + 1
|
||||
binding.animeTrendingViewPager.currentItem =
|
||||
binding.animeTrendingViewPager.currentItem + 1
|
||||
}
|
||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
@@ -150,24 +168,32 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
)
|
||||
|
||||
binding.animeTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.animeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeSeasonsCont.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeTrendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animeTitleContainer.startAnimation(setSlideUp())
|
||||
binding.animeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animeSeasonsCont.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateRecent(adaptor: MediaAdaptor) {
|
||||
binding.animeUpdatedProgressBar.visibility = View.GONE
|
||||
binding.animeUpdatedRecyclerView.adapter = adaptor
|
||||
binding.animeUpdatedRecyclerView.layoutManager =
|
||||
LinearLayoutManager(binding.animeUpdatedRecyclerView.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
LinearLayoutManager(
|
||||
binding.animeUpdatedRecyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeRecently.visibility = View.VISIBLE
|
||||
binding.animeRecently.startAnimation(setSlideUp(uiSettings))
|
||||
binding.animeUpdatedRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeRecently.startAnimation(setSlideUp())
|
||||
binding.animeUpdatedRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animePopular.visibility = View.VISIBLE
|
||||
binding.animePopular.startAnimation(setSlideUp(uiSettings))
|
||||
binding.animePopular.startAnimation(setSlideUp())
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
@@ -177,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.currContext
|
||||
import ani.dantotsu.databinding.FragmentHomeBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.media.user.ListActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
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.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -52,7 +52,11 @@ class HomeFragment : Fragment() {
|
||||
private var _binding: FragmentHomeBinding? = null
|
||||
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)
|
||||
return binding.root
|
||||
}
|
||||
@@ -66,14 +70,13 @@ class HomeFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val scope = lifecycleScope
|
||||
var uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
fun load() {
|
||||
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
|
||||
binding.homeUserName.text = Anilist.username
|
||||
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
||||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
||||
if (!uiSettings.bannerAnimations) binding.homeUserBg.pause()
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
|
||||
binding.homeUserBg.loadImage(Anilist.bg)
|
||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||
|
||||
@@ -94,20 +97,26 @@ class HomeFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||
binding.homeUserDataContainer.visibility = View.VISIBLE
|
||||
binding.homeUserDataContainer.layoutAnimation = LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
|
||||
binding.homeUserDataContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideUp(), 0.25f)
|
||||
binding.homeAnimeList.visibility = View.VISIBLE
|
||||
binding.homeMangaList.visibility = View.VISIBLE
|
||||
binding.homeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.homeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
else {
|
||||
snackString(currContext()?.getString(R.string.please_reload))
|
||||
}
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show(parentFragmentManager, "dialog")
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
|
||||
dialogFragment.show(
|
||||
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
|
||||
"dialog"
|
||||
)
|
||||
}
|
||||
|
||||
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -117,18 +126,18 @@ class HomeFragment : Fragment() {
|
||||
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
var reached = false
|
||||
val duration = (uiSettings.animationSpeed * 200).toLong()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||
reached = true
|
||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration).start()
|
||||
} else {
|
||||
if (reached) {
|
||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration).start()
|
||||
}
|
||||
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
|
||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||
reached = true
|
||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||
.start()
|
||||
} else {
|
||||
if (reached) {
|
||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +147,13 @@ class HomeFragment : Fragment() {
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
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
|
||||
)
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
recyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
|
||||
} else {
|
||||
empty.visibility = View.VISIBLE
|
||||
}
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp(uiSettings))
|
||||
title.startAnimation(setSlideUp())
|
||||
progress.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -278,25 +294,25 @@ class HomeFragment : Fragment() {
|
||||
binding.homeRecommended
|
||||
)
|
||||
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||
|
||||
model.empty.observe(viewLifecycleOwner) {
|
||||
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
|
||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||
binding.homeDantotsuContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeDantotsuContainer.startAnimation(setSlideUp())
|
||||
binding.homeDantotsuIcon.setSafeOnClickListener {
|
||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||
}
|
||||
}
|
||||
|
||||
val array = arrayOf(
|
||||
Runnable { runBlocking { model.setAnimeContinue() } },
|
||||
Runnable { runBlocking { model.setAnimeFav() } },
|
||||
Runnable { runBlocking { model.setAnimePlanned() } },
|
||||
Runnable { runBlocking { model.setMangaContinue() } },
|
||||
Runnable { runBlocking { model.setMangaFav() } },
|
||||
Runnable { runBlocking { model.setMangaPlanned() } },
|
||||
Runnable { runBlocking { model.setRecommendation() } }
|
||||
"AnimeContinue",
|
||||
"AnimeFav",
|
||||
"AnimePlanned",
|
||||
"MangaContinue",
|
||||
"MangaFav",
|
||||
"MangaPlanned",
|
||||
"Recommendation"
|
||||
)
|
||||
|
||||
val containers = arrayOf(
|
||||
@@ -313,7 +329,6 @@ class HomeFragment : Fragment() {
|
||||
live.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
withContext(Dispatchers.IO) {
|
||||
//Get userData First
|
||||
getUserId(requireContext()) {
|
||||
@@ -322,9 +337,13 @@ class HomeFragment : Fragment() {
|
||||
model.loaded = true
|
||||
model.setListImages()
|
||||
var empty = true
|
||||
val homeLayoutShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayoutShow)
|
||||
runBlocking {
|
||||
model.initHomePage()
|
||||
}
|
||||
(array.indices).forEach { i ->
|
||||
if (uiSettings.homeLayoutShow[i]) {
|
||||
array[i].run()
|
||||
if (homeLayoutShow.elementAt(i)) {
|
||||
empty = false
|
||||
} else withContext(Dispatchers.Main) {
|
||||
containers[i].visibility = View.GONE
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.FragmentLoginBinding
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||
import ani.dantotsu.toast
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
||||
class LoginFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentLoginBinding? = null
|
||||
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)
|
||||
return binding.root
|
||||
}
|
||||
@@ -24,5 +37,100 @@ class LoginFragment : Fragment() {
|
||||
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
|
||||
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
|
||||
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
|
||||
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
|
||||
|
||||
val openDocumentLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
val jsonString =
|
||||
requireActivity().contentResolver.openInputStream(uri)?.readBytes()
|
||||
?: throw Exception("Error reading file")
|
||||
val name =
|
||||
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
|
||||
//.sani is encrypted, .ani is not
|
||||
if (name.endsWith(".sani")) {
|
||||
passwordAlertDialog() { password ->
|
||||
if (password != null) {
|
||||
val salt = jsonString.copyOfRange(0, 16)
|
||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||
val decryptedJson = try {
|
||||
PreferenceKeystore.decryptWithPassword(
|
||||
password,
|
||||
encrypted,
|
||||
salt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast("Incorrect password")
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
if (PreferencePackager.unpack(decryptedJson))
|
||||
restartApp()
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
} else if (name.endsWith(".ani")) {
|
||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
||||
if (PreferencePackager.unpack(decryptedJson))
|
||||
restartApp()
|
||||
} else {
|
||||
toast("Invalid file type")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast("Error importing settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.importSettingsButton.setOnClickListener {
|
||||
openDocumentLauncher.launch(arrayOf("*/*"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView =
|
||||
LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null)
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
subtitleTextView?.text = "Enter your password to decrypt the file"
|
||||
|
||||
val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
password.fill('0')
|
||||
dialog.dismiss()
|
||||
callback(null)
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
|
||||
// Override the positive button here
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
|
||||
if (editText?.text?.isNotBlank() == true) {
|
||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
||||
dialog.dismiss()
|
||||
callback(password)
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restartApp() {
|
||||
val intent = Intent(requireActivity(), requireActivity().javaClass)
|
||||
requireActivity().finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,12 +25,12 @@ import ani.dantotsu.connections.anilist.AnilistMangaViewModel
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.ProgressAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -43,12 +43,15 @@ import kotlin.math.min
|
||||
class MangaFragment : Fragment() {
|
||||
private var _binding: FragmentMangaBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private lateinit var mangaPageAdapter: MangaPageAdapter
|
||||
|
||||
val model: AnilistMangaViewModel by activityViewModels()
|
||||
|
||||
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)
|
||||
return binding.root
|
||||
}
|
||||
@@ -85,7 +88,7 @@ class MangaFragment : Fragment() {
|
||||
|
||||
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
|
||||
|
||||
val mangaPageAdapter = MangaPageAdapter()
|
||||
mangaPageAdapter = MangaPageAdapter()
|
||||
var loading = true
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
@@ -100,7 +103,8 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
|
||||
val progressAdaptor = ProgressAdapter(searched = model.searched)
|
||||
binding.mangaPageRecyclerView.adapter = ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
||||
binding.mangaPageRecyclerView.adapter =
|
||||
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
||||
val layout = LinearLayoutManager(requireContext())
|
||||
binding.mangaPageRecyclerView.layoutManager = layout
|
||||
|
||||
@@ -167,7 +171,7 @@ class MangaFragment : Fragment() {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrending(
|
||||
MediaAdaptor(
|
||||
if (uiSettings.smallView) 3 else 2,
|
||||
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
|
||||
it,
|
||||
requireActivity(),
|
||||
viewPager = mangaPageAdapter.trendingViewPager
|
||||
@@ -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.loadTrending()
|
||||
model.loadTrendingNovel()
|
||||
model.loadPopular("MANGA", sort = Anilist.sortBy[1])
|
||||
model.loadPopular(
|
||||
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularMangaList
|
||||
)
|
||||
)
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.mangaRefresh?.isRefreshing = false
|
||||
@@ -244,6 +253,11 @@ class MangaFragment : Fragment() {
|
||||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
|
||||
//make sure mangaPageAdapter is initialized
|
||||
if (mangaPageAdapter.trendingViewPager != null) {
|
||||
binding.root.requestApplyInsets()
|
||||
binding.root.requestLayout()
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package ani.dantotsu.home
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -15,13 +16,13 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.MediaPageTransformer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.px
|
||||
@@ -29,7 +30,8 @@ import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -40,10 +42,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
|
||||
val binding = ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return MangaPageViewHolder(binding)
|
||||
}
|
||||
|
||||
@@ -53,14 +55,21 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||
val materialCardView =
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
|
||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = (-108f).px
|
||||
}
|
||||
|
||||
@@ -76,7 +85,9 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
|
||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
binding.mangaSearchBar.setEndIconOnClickListener {
|
||||
@@ -104,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 ->
|
||||
onIncludeListClick.invoke(isChecked)
|
||||
}
|
||||
|
||||
PrefManager.setVal(PrefName.PopularMangaList, isChecked)
|
||||
}
|
||||
if (ready.value == false)
|
||||
ready.postValue(true)
|
||||
}
|
||||
|
||||
lateinit var onIncludeListClick : ((Boolean)->Unit)
|
||||
lateinit var onIncludeListClick: ((Boolean) -> Unit)
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
@@ -129,7 +145,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.mangaTrendingViewPager.currentItem = binding.mangaTrendingViewPager.currentItem + 1
|
||||
binding.mangaTrendingViewPager.currentItem =
|
||||
binding.mangaTrendingViewPager.currentItem + 1
|
||||
}
|
||||
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
@@ -141,23 +158,30 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
)
|
||||
|
||||
binding.mangaTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.mangaListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaTrendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.mangaTitleContainer.startAnimation(setSlideUp())
|
||||
binding.mangaListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateNovel(adaptor: MediaAdaptor) {
|
||||
binding.mangaNovelProgressBar.visibility = View.GONE
|
||||
binding.mangaNovelRecyclerView.adapter = adaptor
|
||||
binding.mangaNovelRecyclerView.layoutManager =
|
||||
LinearLayoutManager(binding.mangaNovelRecyclerView.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
LinearLayoutManager(
|
||||
binding.mangaNovelRecyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.mangaNovel.visibility = View.VISIBLE
|
||||
binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
|
||||
binding.mangaNovelRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaNovel.startAnimation(setSlideUp())
|
||||
binding.mangaNovelRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.mangaPopular.visibility = View.VISIBLE
|
||||
binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
|
||||
binding.mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
@@ -167,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,31 +1,115 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.download.anime.OfflineAnimeFragment
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.offline.OfflineFragment
|
||||
import ani.dantotsu.selectedOption
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
|
||||
class NoInternet : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityNoInternetBinding
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
val binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||
binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
bottomBar.background = backgroundDrawable
|
||||
}
|
||||
binding.refreshButton.setOnClickListener {
|
||||
if (isOnline(this)) {
|
||||
startMainActivity(this)
|
||||
bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
|
||||
var doubleBackToExitPressedOnce = false
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (doubleBackToExitPressedOnce) {
|
||||
finishAffinity()
|
||||
}
|
||||
doubleBackToExitPressedOnce = true
|
||||
snackString(this@NoInternet.getString(R.string.back_to_exit))
|
||||
Handler(Looper.getMainLooper()).postDelayed(
|
||||
{ doubleBackToExitPressedOnce = false },
|
||||
2000
|
||||
)
|
||||
}
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
selectedOption = PrefManager.getVal(PrefName.DefaultStartUpTab)
|
||||
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
ani.dantotsu.bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
|
||||
//supportFragmentManager.beginTransaction().replace(binding.fragmentContainer.id, OfflineFragment()).commit()
|
||||
|
||||
}
|
||||
|
||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> OfflineAnimeFragment()
|
||||
2 -> OfflineMangaFragment()
|
||||
else -> OfflineFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,15 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
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.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -29,6 +35,7 @@ class AuthorActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -4,19 +4,23 @@ import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
@@ -33,37 +37,51 @@ class CalendarActivity : AppCompatActivity() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
|
||||
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, typedValue, true)
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
val primaryColor = typedValue.data
|
||||
val typedValue2 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true)
|
||||
val primaryTextColor = typedValue2.data
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue2,
|
||||
true
|
||||
)
|
||||
val titleTextColor = typedValue2.data
|
||||
val typedValue3 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue3, true)
|
||||
val secondaryColor = typedValue3.data
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||
val primaryTextColor = typedValue3.data
|
||||
val typedValue4 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
|
||||
val secondaryTextColor = typedValue4.data
|
||||
|
||||
window.statusBarColor = primaryColor
|
||||
window.navigationBarColor = primaryColor
|
||||
binding.listTabLayout.setBackgroundColor(primaryColor)
|
||||
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
binding.listTabLayout.setTabTextColors(primaryTextColor, primaryTextColor)
|
||||
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) {
|
||||
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
|
||||
this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||
binding.root.fitsSystemWindows = true
|
||||
|
||||
}else{
|
||||
} else {
|
||||
binding.root.fitsSystemWindows = false
|
||||
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)
|
||||
|
||||
@@ -74,14 +92,15 @@ class CalendarActivity : AppCompatActivity() {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
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) {
|
||||
if (it != null) {
|
||||
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 values = it.values.toList()
|
||||
val savedTab = this.selectedTabIdx
|
||||
@@ -103,4 +122,4 @@ class CalendarActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,26 +11,23 @@ import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.databinding.ItemCharacterBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import java.io.Serializable
|
||||
|
||||
class CharacterAdapter(
|
||||
private val characterList: ArrayList<Character>
|
||||
) : RecyclerView.Adapter<CharacterAdapter.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)
|
||||
}
|
||||
|
||||
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root, uiSettings)
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val character = characterList[position]
|
||||
binding.itemCompactRelation.text = character.role + " "
|
||||
binding.itemCompactImage.loadImage(character.image)
|
||||
@@ -38,16 +35,23 @@ class CharacterAdapter(
|
||||
}
|
||||
|
||||
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 {
|
||||
itemView.setOnClickListener {
|
||||
val char = characterList[bindingAdapterPosition]
|
||||
ContextCompat.startActivity(
|
||||
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(
|
||||
itemView.context as Activity,
|
||||
Pair.create(binding.itemCompactImage, ViewCompat.getTransitionName(binding.itemCompactImage)!!),
|
||||
Pair.create(
|
||||
binding.itemCompactImage,
|
||||
ViewCompat.getTransitionName(binding.itemCompactImage)!!
|
||||
),
|
||||
).toBundle()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,11 +13,18 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
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.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 com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -30,19 +37,21 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
private lateinit var character: Character
|
||||
private var loaded = false
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
initActivity(this)
|
||||
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 }
|
||||
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
@@ -59,7 +68,13 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
binding.characterTitle.text = character.name
|
||||
banner.loadImage(character.banner)
|
||||
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) {
|
||||
if (it != null && !loaded) {
|
||||
@@ -71,14 +86,15 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
val roles = character.roles
|
||||
if (roles != null) {
|
||||
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 gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (position) {
|
||||
0 -> gridSize
|
||||
0 -> gridSize
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
@@ -116,16 +132,19 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
binding.characterCover.scaleY = 1f * 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) {
|
||||
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)
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
|
||||
RecyclerView.Adapter<CharacterDetailsAdapter.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)
|
||||
}
|
||||
|
||||
@@ -23,20 +24,22 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val desc =
|
||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
||||
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
||||
(if (character.gender != "null") 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)
|
||||
"Female" -> currActivity()!!.getString(R.string.female)
|
||||
else -> character.gender
|
||||
} else "") + "\n" + character.description
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
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,8 +12,9 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -26,6 +27,7 @@ class GenreActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityGenreBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -48,10 +50,13 @@ class GenreActivity : AppCompatActivity() {
|
||||
model.doneListener?.invoke()
|
||||
}
|
||||
binding.mediaInfoGenresRecyclerView.adapter = adapter
|
||||
binding.mediaInfoGenresRecyclerView.layoutManager = GridLayoutManager(this, (screenWidth / 156f).toInt())
|
||||
binding.mediaInfoGenresRecyclerView.layoutManager =
|
||||
GridLayoutManager(this, (screenWidth / 156f).toInt())
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {
|
||||
model.loadGenres(
|
||||
Anilist.genres ?: loadLocalGenres() ?: arrayListOf()
|
||||
) {
|
||||
MainScope().launch {
|
||||
adapter.addGenre(it)
|
||||
}
|
||||
@@ -59,4 +64,14 @@ class GenreActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLocalGenres(): ArrayList<String>? {
|
||||
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||
.toMutableList() as ArrayList<String>?
|
||||
return if (genres.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
genres
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,8 @@ class GenreAdapter(
|
||||
}
|
||||
|
||||
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 {
|
||||
itemView.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
@@ -48,15 +49,15 @@ class GenreAdapter(
|
||||
.putExtra("sortBy", Anilist.sortBy[2])
|
||||
.putExtra("search", true)
|
||||
.also {
|
||||
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
|
||||
if (!Anilist.adult) Toast.makeText(
|
||||
itemView.context,
|
||||
currActivity()?.getString(R.string.content_18),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
it.putExtra("hentai", true)
|
||||
}
|
||||
},
|
||||
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
|
||||
if (!Anilist.adult) Toast.makeText(
|
||||
itemView.context,
|
||||
currActivity()?.getString(R.string.content_18),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
it.putExtra("hentai", true)
|
||||
}
|
||||
},
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.MediaEdge
|
||||
import ani.dantotsu.connections.anilist.api.MediaList
|
||||
@@ -22,7 +23,7 @@ data class Media(
|
||||
val userPreferredName: String,
|
||||
|
||||
var cover: String? = null,
|
||||
val banner: String? = null,
|
||||
var banner: String? = null,
|
||||
var relation: String? = null,
|
||||
var popularity: Int? = null,
|
||||
|
||||
@@ -40,7 +41,7 @@ data class Media(
|
||||
var userUpdatedAt: Long? = null,
|
||||
var userStartedAt: FuzzyDate = FuzzyDate(),
|
||||
var userCompletedAt: FuzzyDate = FuzzyDate(),
|
||||
var inCustomListsOf: MutableMap<String, Boolean>?= null,
|
||||
var inCustomListsOf: MutableMap<String, Boolean>? = null,
|
||||
var userFavOrder: Int? = null,
|
||||
|
||||
val status: String? = null,
|
||||
@@ -69,7 +70,7 @@ data class Media(
|
||||
var shareLink: String? = null,
|
||||
var selected: Selected? = null,
|
||||
|
||||
var idKitsu: String?=null,
|
||||
var idKitsu: String? = null,
|
||||
|
||||
var cameFromContinue: Boolean = false
|
||||
) : Serializable {
|
||||
@@ -117,6 +118,20 @@ data class Media(
|
||||
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
|
||||
}
|
||||
|
||||
fun emptyMedia() = Media(
|
||||
id = 0,
|
||||
name = "No media found",
|
||||
nameRomaji = "No media found",
|
||||
userPreferredName = "",
|
||||
isAdult = false,
|
||||
isFav = false,
|
||||
isListPrivate = false,
|
||||
userScore = 0,
|
||||
userStatus = "",
|
||||
format = "",
|
||||
)
|
||||
|
||||
object MediaSingleton {
|
||||
var media: Media? = null
|
||||
var bitmap: Bitmap? = null
|
||||
}
|
||||
|
||||
@@ -4,12 +4,19 @@ import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -19,7 +26,8 @@ import ani.dantotsu.databinding.ItemMediaCompactBinding
|
||||
import ani.dantotsu.databinding.ItemMediaLargeBinding
|
||||
import ani.dantotsu.databinding.ItemMediaPageBinding
|
||||
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
@@ -37,20 +45,40 @@ class MediaAdaptor(
|
||||
private val viewPager: ViewPager2? = null,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (type) {
|
||||
0 -> MediaViewHolder(ItemMediaCompactBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
1 -> MediaLargeViewHolder(ItemMediaLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
2 -> MediaPageViewHolder(ItemMediaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
3 -> MediaPageSmallViewHolder(
|
||||
0 -> MediaViewHolder(
|
||||
ItemMediaCompactBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
1 -> MediaLargeViewHolder(
|
||||
ItemMediaLargeBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
2 -> MediaPageViewHolder(
|
||||
ItemMediaPageBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
3 -> MediaPageSmallViewHolder(
|
||||
ItemMediaPageSmallBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
@@ -61,14 +89,16 @@ class MediaAdaptor(
|
||||
when (type) {
|
||||
0 -> {
|
||||
val b = (holder as MediaViewHolder).binding
|
||||
setAnimation(activity, b.root, uiSettings)
|
||||
setAnimation(activity, b.root)
|
||||
val media = mediaList?.getOrNull(position)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
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.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.root.context,
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
@@ -100,29 +130,37 @@ class MediaAdaptor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1 -> {
|
||||
val b = (holder as MediaLargeViewHolder).binding
|
||||
setAnimation(activity, b.root, uiSettings)
|
||||
setAnimation(activity, b.root)
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400)
|
||||
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
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.root.context,
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
)
|
||||
if (media.anime != null) {
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
|
||||
else currActivity()!!.getString(R.string.episode_singular)
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.episode_plural)
|
||||
else currActivity()!!.getString(R.string.episode_singular)
|
||||
b.itemCompactTotal.text =
|
||||
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) {
|
||||
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)
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@@ -133,19 +171,22 @@ class MediaAdaptor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val b = (holder as MediaPageViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
if (uiSettings.bannerAnimations)
|
||||
if (bannerAnimations)
|
||||
b.itemCompactBanner.setTransitionGenerator(
|
||||
RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed)) as Float)).toLong(),
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val banner =
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
@@ -153,22 +194,29 @@ class MediaAdaptor(
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||
.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.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.root.context,
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
)
|
||||
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)
|
||||
b.itemCompactTotal.text =
|
||||
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) {
|
||||
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)
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@@ -180,19 +228,22 @@ class MediaAdaptor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3 -> {
|
||||
val b = (holder as MediaPageSmallViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
if (uiSettings.bannerAnimations)
|
||||
if (bannerAnimations)
|
||||
b.itemCompactBanner.setTransitionGenerator(
|
||||
RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val banner =
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
@@ -200,10 +251,12 @@ class MediaAdaptor(
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||
.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.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.root.context,
|
||||
(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 ?: ""
|
||||
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)
|
||||
b.itemCompactTotal.text =
|
||||
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) {
|
||||
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)
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@@ -245,61 +303,161 @@ class MediaAdaptor(
|
||||
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 {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
itemView.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
binding.itemCompactImage.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
itemView.setOnTouchListener { _, _ -> true }
|
||||
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
binding.itemCompactTitleContainer.setSafeOnClickListener { clicked(bindingAdapterPosition) }
|
||||
binding.itemCompactImage.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
binding.itemCompactTitleContainer.setSafeOnClickListener {
|
||||
clicked(
|
||||
bindingAdapterPosition,
|
||||
binding.itemCompactImage,
|
||||
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
|
||||
)
|
||||
}
|
||||
itemView.setOnTouchListener { _, _ -> true }
|
||||
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) {
|
||||
val media = mediaList?.get(position)
|
||||
if (bitmap != null) MediaSingleton.bitmap = bitmap
|
||||
ContextCompat.startActivity(
|
||||
activity,
|
||||
Intent(activity, MediaDetailsActivity::class.java).putExtra(
|
||||
"media",
|
||||
media as Serializable
|
||||
), null
|
||||
),
|
||||
if (itemCompactImage != null) {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
activity,
|
||||
itemCompactImage,
|
||||
ViewCompat.getTransitionName(itemCompactImage)!!
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun longClicked(position: Int): Boolean {
|
||||
if ((mediaList?.size ?: 0) > position && position != -1) {
|
||||
val media = mediaList?.get(position) ?: return false
|
||||
if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
|
||||
MediaListDialogSmallFragment.newInstance(media).show(activity.supportFragmentManager, "list")
|
||||
MediaListDialogSmallFragment.newInstance(media)
|
||||
.show(activity.supportFragmentManager, "list")
|
||||
return true
|
||||
}
|
||||
}
|
||||
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,20 +31,19 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.media.manga.MangaReadFragment
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
@@ -64,7 +63,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
private val scope = lifecycleScope
|
||||
private val model: MediaDetailsViewModel by viewModels()
|
||||
private lateinit var tabLayout: NavigationBarView
|
||||
private lateinit var uiSettings: UserInterfaceSettings
|
||||
var selected = 0
|
||||
var anime = true
|
||||
private var adult = false
|
||||
@@ -72,7 +70,16 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||
if (media.name == "No media found") {
|
||||
snackString(media.name)
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return
|
||||
}
|
||||
mediaSingleton = null
|
||||
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||
MediaSingleton.bitmap = null
|
||||
|
||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
@@ -80,13 +87,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
//Ui init
|
||||
|
||||
initActivity(this)
|
||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||
|
||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
||||
|
||||
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -102,23 +107,25 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
if (uiSettings.bannerAnimations) {
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
if (bannerAnimations) {
|
||||
val adi = AccelerateDecelerateInterpolator()
|
||||
val generator = RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
|
||||
adi
|
||||
)
|
||||
binding.mediaBanner.setTransitionGenerator(generator)
|
||||
}
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||
val viewPager = binding.mediaViewPager
|
||||
tabLayout = binding.mediaTab as NavigationBarView
|
||||
viewPager.isUserInputEnabled = false
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
|
||||
var media: Media = intent.getSerialized("media") ?: return
|
||||
media.selected = model.loadSelected(media)
|
||||
|
||||
val isDownload = intent.getBooleanExtra("download", false)
|
||||
media.selected = model.loadSelected(media, isDownload)
|
||||
|
||||
binding.mediaCoverImage.loadImage(media.cover)
|
||||
binding.mediaCoverImage.setOnLongClickListener {
|
||||
@@ -131,7 +138,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
banner.loadImage(media.banner ?: media.cover, 400)
|
||||
val gestureDetector = GestureDetector(this, object : GesturesListener() {
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
if (!uiSettings.bannerAnimations)
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
|
||||
snackString(getString(R.string.enable_banner_animations))
|
||||
else {
|
||||
binding.mediaBanner.restart()
|
||||
@@ -149,7 +156,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
})
|
||||
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
||||
binding.mediaTitle.text = media.userPreferredName
|
||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||
binding.mediaTitle.text = " ${media.userPreferredName}"
|
||||
binding.incognito.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.mediaTitle.text = media.userPreferredName
|
||||
}
|
||||
binding.mediaTitle.setOnLongClickListener {
|
||||
copyToClipboard(media.userPreferredName)
|
||||
true
|
||||
@@ -268,7 +280,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
} else snackString(getString(R.string.please_login_anilist))
|
||||
}
|
||||
binding.mediaAddToList.setOnLongClickListener {
|
||||
saveData("${media.id}_progressDialog", true)
|
||||
PrefManager.setCustomVal(
|
||||
"${media.id}_progressDialog",
|
||||
true,
|
||||
)
|
||||
snackString(getString(R.string.auto_update_reset))
|
||||
true
|
||||
}
|
||||
@@ -300,7 +315,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
|
||||
adult = media.isAdult
|
||||
|
||||
tabLayout.menu.clear()
|
||||
if (media.anime != null) {
|
||||
viewPager.adapter =
|
||||
@@ -312,7 +326,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
lifecycle,
|
||||
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
|
||||
)
|
||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||
if (media.format == "NOVEL") {
|
||||
tabLayout.inflateMenu(R.menu.novel_menu_detail)
|
||||
} else {
|
||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||
}
|
||||
anime = false
|
||||
}
|
||||
|
||||
@@ -324,9 +342,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
tabLayout.setOnItemSelectedListener { item ->
|
||||
selectFromID(item.itemId)
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
val sel = model.loadSelected(media)
|
||||
val sel = model.loadSelected(media, isDownload)
|
||||
sel.window = selected
|
||||
model.saveSelected(media.id, sel, this)
|
||||
model.saveSelected(media.id, sel)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -335,7 +353,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
|
||||
if (model.continueMedia == null && media.cameFromContinue) {
|
||||
model.continueMedia = loadData("continue_media") ?: true
|
||||
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
||||
selected = 1
|
||||
}
|
||||
|
||||
@@ -376,7 +394,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
tabLayout.selectedItemId = idFromSelect()
|
||||
if (this::tabLayout.isInitialized) {
|
||||
tabLayout.selectedItemId = idFromSelect()
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@@ -418,7 +438,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
binding.mediaCover.visibility =
|
||||
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
val duration = (200 * uiSettings.animationSpeed).toLong()
|
||||
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
|
||||
val typedValue = TypedValue()
|
||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSecondary,
|
||||
@@ -437,7 +457,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
|
||||
.setDuration(duration).start()
|
||||
binding.mediaBanner.pause()
|
||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
@@ -449,8 +468,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
.start()
|
||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.mediaBanner.resume()
|
||||
}
|
||||
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
|
||||
false
|
||||
@@ -474,9 +492,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
init {
|
||||
enabled(true)
|
||||
scope.launch {
|
||||
clicked()
|
||||
}
|
||||
image.setOnClickListener {
|
||||
if (pressable && !disabled) {
|
||||
pressable = false
|
||||
@@ -527,5 +542,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
image.alpha = if (disabled) 0.33f else 1f
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var mediaSingleton: Media? = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +1,64 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.R
|
||||
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.SelectorDialogFragment
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.others.AniSkip
|
||||
import ani.dantotsu.others.Jikan
|
||||
import ani.dantotsu.others.Kitsu
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.Book
|
||||
import ani.dantotsu.parsers.MangaImage
|
||||
import ani.dantotsu.parsers.MangaReadSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.AniyomiAdapter
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MediaDetailsViewModel : ViewModel() {
|
||||
val scrolledToTop = MutableLiveData(true)
|
||||
|
||||
fun saveSelected(id: Int, data: Selected, activity: Activity? = null) {
|
||||
saveData("$id-select", data, activity)
|
||||
fun saveSelected(id: Int, data: Selected) {
|
||||
PrefManager.setCustomVal("Selected-$id", data)
|
||||
}
|
||||
|
||||
|
||||
fun loadSelected(media: Media): Selected {
|
||||
val sharedPreferences = Injekt.get<SharedPreferences>()
|
||||
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
|
||||
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
|
||||
true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
|
||||
else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
|
||||
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
|
||||
val data =
|
||||
PrefManager.getNullableCustomVal("Selected-${media.id}", null, Selected::class.java)
|
||||
?: Selected().let {
|
||||
it.sourceIndex = 0
|
||||
it.preferDub = PrefManager.getVal(PrefName.SettingsPreferDub)
|
||||
saveSelected(media.id, it)
|
||||
it
|
||||
}
|
||||
if (isDownload) {
|
||||
data.sourceIndex = if (media.anime != null) {
|
||||
AnimeSources.list.size - 1
|
||||
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
||||
MangaSources.list.size - 1
|
||||
} else {
|
||||
NovelSources.list.size - 1
|
||||
}
|
||||
it.preferDub = loadData("settings_prefer_dub") ?: false
|
||||
saveSelected(media.id, it)
|
||||
it
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -70,7 +66,9 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
fun loadSelectedStringLocation(sourceName: String): Int {
|
||||
//find the location of the source in the list
|
||||
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
||||
if (location == -1) {location = 0}
|
||||
if (location == -1) {
|
||||
location = 0
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
@@ -95,7 +93,9 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
|
||||
//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
|
||||
suspend fun loadKitsuEpisodes(s: Media) {
|
||||
tryWithSuspend {
|
||||
@@ -103,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
|
||||
suspend fun loadFillerEpisodes(s: Media) {
|
||||
tryWithSuspend {
|
||||
@@ -120,8 +122,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
|
||||
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
|
||||
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
|
||||
suspend fun loadEpisodes(media: Media, i: Int) {
|
||||
if (!epsLoaded.containsKey(i)) {
|
||||
suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) {
|
||||
if (!epsLoaded.containsKey(i) || invalidate) {
|
||||
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
|
||||
}
|
||||
episodes.postValue(epsLoaded)
|
||||
@@ -134,7 +136,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -173,7 +176,12 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
|
||||
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
|
||||
episodeNum ?: return
|
||||
if (timeStampsMap.containsKey(episodeNum))
|
||||
@@ -183,7 +191,11 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
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()) {
|
||||
|
||||
val server = selected.server ?: return false
|
||||
@@ -193,8 +205,10 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
selected.sourceIndex = selected.sourceIndex
|
||||
if (!post && !it.allowsPreloading) null
|
||||
else ep.sEpisode?.let { it1 ->
|
||||
it.loadSingleVideoServer(server, link, ep.extra,
|
||||
it1, post)
|
||||
it.loadSingleVideoServer(
|
||||
server, link, ep.extra,
|
||||
it1, post
|
||||
)
|
||||
}
|
||||
} ?: return false)
|
||||
ep.allStreams = false
|
||||
@@ -217,7 +231,14 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
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 {
|
||||
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
|
||||
if (media.anime?.episodes?.get(i) != null) {
|
||||
@@ -227,23 +248,32 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
return@post
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Manga
|
||||
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>>()
|
||||
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters
|
||||
suspend fun loadMangaChapters(media: Media, i: Int) {
|
||||
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> =
|
||||
mangaChapters
|
||||
|
||||
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
||||
logger("Loading Manga Chapters : $mangaLoaded")
|
||||
if (!mangaLoaded.containsKey(i)) tryWithSuspend {
|
||||
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
||||
mangaLoaded[i] =
|
||||
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||
}
|
||||
mangaChapters.postValue(mangaLoaded)
|
||||
}
|
||||
@@ -258,10 +288,17 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
||||
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
||||
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
|
||||
suspend fun loadMangaChapterImages(
|
||||
chapter: MangaChapter,
|
||||
selected: Selected,
|
||||
series: String,
|
||||
post: Boolean = true
|
||||
): Boolean {
|
||||
|
||||
return tryWithSuspend(true) {
|
||||
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)
|
||||
true
|
||||
@@ -269,13 +306,15 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
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 novelResponses = MutableLiveData<List<ShowResponse>>(null)
|
||||
suspend fun searchNovels(query: String, i: Int) {
|
||||
val source = novelSources[i]
|
||||
val position = if (i >= novelSources.list.size) 0 else i
|
||||
val source = novelSources[position]
|
||||
tryWithSuspend(post = true) {
|
||||
if (source != null) {
|
||||
novelResponses.postValue(source.search(query))
|
||||
@@ -284,7 +323,7 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
suspend fun autoSearchNovels(media: Media) {
|
||||
val source = novelSources[media.selected?.sourceIndex?:0]
|
||||
val source = novelSources[media.selected?.sourceIndex ?: 0]
|
||||
tryWithSuspend(post = true) {
|
||||
if (source != null) {
|
||||
novelResponses.postValue(source.sortedSearch(media))
|
||||
@@ -295,7 +334,9 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
val book: MutableLiveData<Book> = MutableLiveData(null)
|
||||
suspend fun loadBook(novel: ShowResponse, i: Int) {
|
||||
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.GenresViewModel
|
||||
import ani.dantotsu.databinding.*
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -43,7 +45,11 @@ class MediaInfoFragment : Fragment() {
|
||||
private var type = "ANIME"
|
||||
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)
|
||||
return binding.root
|
||||
}
|
||||
@@ -55,12 +61,13 @@ class MediaInfoFragment : Fragment() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val model: MediaDetailsViewModel by activityViewModels()
|
||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||
|
||||
model.scrolledToTop.observe(viewLifecycleOwner){
|
||||
if(it) binding.mediaInfoScroll.scrollTo(0,0)
|
||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||
if (it) binding.mediaInfoScroll.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
@@ -68,56 +75,85 @@ class MediaInfoFragment : Fragment() {
|
||||
loaded = true
|
||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||
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 {
|
||||
copyToClipboard(media.name?:media.nameRomaji)
|
||||
copyToClipboard(media.name ?: media.nameRomaji)
|
||||
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.setOnLongClickListener {
|
||||
copyToClipboard(media.nameRomaji)
|
||||
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.mediaInfoFormat.text = media.format
|
||||
binding.mediaInfoSource.text = media.source
|
||||
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
||||
binding.mediaInfoEnd.text =media.endDate?.toString() ?: "??"
|
||||
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
||||
if (media.anime != null) {
|
||||
binding.mediaInfoDuration.text =
|
||||
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
|
||||
val episodeDuration = media.anime.episodeDuration
|
||||
|
||||
binding.mediaInfoDuration.text = when {
|
||||
episodeDuration != null -> {
|
||||
val hours = episodeDuration / 60
|
||||
val minutes = episodeDuration % 60
|
||||
|
||||
val formattedDuration = buildString {
|
||||
if (hours > 0) {
|
||||
append("$hours hour")
|
||||
if (hours > 1) append("s")
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
if (hours > 0) append(", ")
|
||||
append("$minutes min")
|
||||
if (minutes > 1) append("s")
|
||||
}
|
||||
}
|
||||
|
||||
formattedDuration
|
||||
}
|
||||
|
||||
else -> "??"
|
||||
}
|
||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeason.text =
|
||||
(media.anime.season ?: "??")+ " " + (media.anime.seasonYear ?: "??")
|
||||
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
|
||||
if (media.anime.mainStudio != null) {
|
||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, StudioActivity::class.java).putExtra(
|
||||
"studio",
|
||||
media.anime.mainStudio!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoStudioContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, StudioActivity::class.java).putExtra(
|
||||
"studio",
|
||||
media.anime.mainStudio!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (media.anime.author != null) {
|
||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoAuthor.text = media.anime.author!!.name
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.anime.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.anime.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||
@@ -131,15 +167,17 @@ class MediaInfoFragment : Fragment() {
|
||||
if (media.manga.author != null) {
|
||||
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoAuthor.text = media.manga.author!!.name
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.manga.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
if (!offline) {
|
||||
binding.mediaInfoAuthorContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(activity, AuthorActivity::class.java).putExtra(
|
||||
"author",
|
||||
media.manga.author!! as Serializable
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +221,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (media.trailer != null) {
|
||||
if (media.trailer != null && !offline) {
|
||||
@Suppress("DEPRECATION")
|
||||
class MyChrome : WebChromeClient() {
|
||||
private var mCustomView: View? = null
|
||||
@@ -237,7 +275,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) {
|
||||
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty()) && !offline) {
|
||||
val markWon = Markwon.builder(requireContext())
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
|
||||
@@ -246,7 +284,12 @@ class MediaInfoFragment : Fragment() {
|
||||
val end = a.indexOf('"', first).let { if (it != -1) it else return a }
|
||||
val name = a.subSequence(first, end).toString()
|
||||
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)}"
|
||||
}
|
||||
|
||||
@@ -270,7 +313,11 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
|
||||
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)
|
||||
makeText(bind.itemText, media.anime.op)
|
||||
parent.addView(bind.root)
|
||||
@@ -278,14 +325,18 @@ class MediaInfoFragment : Fragment() {
|
||||
|
||||
|
||||
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)
|
||||
makeText(bind.itemText, media.anime.ed)
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
}
|
||||
|
||||
if (media.genres.isNotEmpty()) {
|
||||
if (media.genres.isNotEmpty() && !offline) {
|
||||
val bind = ActivityGenreBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
@@ -316,7 +367,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (media.tags.isNotEmpty()) {
|
||||
if (media.tags.isNotEmpty() && !offline) {
|
||||
val bind = ItemTitleChipgroupBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
@@ -357,7 +408,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.characters.isNullOrEmpty()) {
|
||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
@@ -374,7 +425,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.relations.isNullOrEmpty()) {
|
||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||
if (media.sequel != null || media.prequel != null) {
|
||||
val bind = ItemQuelsBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
@@ -437,7 +488,7 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bindi.root)
|
||||
}
|
||||
|
||||
if (!media.recommendations.isNullOrEmpty()) {
|
||||
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
@@ -458,7 +509,8 @@ class MediaInfoFragment : Fragment() {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
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
|
||||
cornerTop.start()
|
||||
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->
|
||||
|
||||
@@ -14,9 +14,9 @@ import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||
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.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -27,7 +27,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetMediaListBinding? = null
|
||||
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)
|
||||
return binding.root
|
||||
}
|
||||
@@ -46,9 +50,13 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListLayout.visibility = View.VISIBLE
|
||||
|
||||
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 userStatus = if(media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
|
||||
|
||||
val statusStrings =
|
||||
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.setAdapter(
|
||||
ArrayAdapter(
|
||||
@@ -160,7 +168,9 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
val init =
|
||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||
.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)) {
|
||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||
onComplete()
|
||||
@@ -186,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
media?.inCustomListsOf?.forEach {
|
||||
SwitchMaterial(requireContext()).apply {
|
||||
MaterialSwitch(requireContext()).apply {
|
||||
isChecked = it.value
|
||||
text = it.key
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
@@ -201,11 +211,15 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (media != null) {
|
||||
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 status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||
val rewatch = _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
|
||||
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()
|
||||
?.times(10))?.toInt()
|
||||
val status =
|
||||
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
|
||||
val rewatch =
|
||||
_binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
|
||||
val notes = _binding?.mediaListNotes?.text?.toString()
|
||||
val startD = start.date
|
||||
val endD = end.date
|
||||
@@ -245,7 +259,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Anilist.mutation.deleteList(id)
|
||||
MAL.query.deleteList(media?.anime!=null,media?.idMAL)
|
||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||
}
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
|
||||
@@ -12,10 +12,9 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -45,7 +44,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetMediaListSmallBinding? = null
|
||||
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)
|
||||
return binding.root
|
||||
}
|
||||
@@ -59,8 +62,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListProgressBar.visibility = View.GONE
|
||||
binding.mediaListLayout.visibility = View.VISIBLE
|
||||
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 userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
||||
val statusStrings =
|
||||
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.setAdapter(
|
||||
@@ -129,10 +136,26 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
withContext(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
|
||||
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
|
||||
val 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)
|
||||
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()
|
||||
?.times(10))?.toInt()
|
||||
val 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()
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class OtherDetailsViewModel : ViewModel() {
|
||||
private val character: MutableLiveData<Character> = MutableLiveData(null)
|
||||
@@ -19,26 +19,28 @@ class OtherDetailsViewModel : ViewModel() {
|
||||
suspend fun loadStudio(m: Studio) {
|
||||
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
|
||||
}
|
||||
|
||||
private val author: MutableLiveData<Author> = MutableLiveData(null)
|
||||
fun getAuthor(): LiveData<Author> = author
|
||||
suspend fun loadAuthor(m: Author) {
|
||||
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() {
|
||||
val curr = System.currentTimeMillis()/1000
|
||||
val res = Anilist.query.recentlyUpdated(false,curr-86400,curr+(86400*6))
|
||||
val curr = System.currentTimeMillis() / 1000
|
||||
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
|
||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||
val map = mutableMapOf<String,MutableList<Media>>()
|
||||
val idMap = mutableMapOf<String,MutableList<Int>>()
|
||||
val map = mutableMapOf<String, MutableList<Media>>()
|
||||
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||
res?.forEach {
|
||||
val v = it.relation?.split(",")?.map { i-> i.toLong() }!!
|
||||
val dateInfo = df.format(Date(v[1]*1000))
|
||||
val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
|
||||
val dateInfo = df.format(Date(v[1] * 1000))
|
||||
val list = map.getOrPut(dateInfo) { mutableListOf() }
|
||||
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
|
||||
it.relation = "Episode ${v[0]}"
|
||||
if(!idList.contains(it.id)) {
|
||||
if (!idList.contains(it.id)) {
|
||||
idList.add(it.id)
|
||||
list.add(it)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
||||
var bar: ProgressBar? = null
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -33,7 +34,12 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
||||
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -51,7 +57,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
||||
}
|
||||
|
||||
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 {
|
||||
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -32,12 +34,14 @@ class SearchActivity : AppCompatActivity() {
|
||||
private lateinit var mediaAdaptor: MediaAdaptor
|
||||
private lateinit var progressAdapter: ProgressAdapter
|
||||
private lateinit var concatAdapter: ConcatAdapter
|
||||
private lateinit var headerAdaptor: SearchAdapter
|
||||
|
||||
lateinit var result: SearchResults
|
||||
lateinit var updateChips: (()->Unit)
|
||||
lateinit var updateChips: (() -> Unit)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivitySearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -49,7 +53,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
bottom = navBarHeight + 80f.px
|
||||
)
|
||||
|
||||
style = loadData<Int>("searchStyle") ?: 0
|
||||
style = PrefManager.getVal(PrefName.SearchStyle)
|
||||
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
|
||||
if (!listOnly!!) listOnly = null
|
||||
|
||||
@@ -74,17 +78,17 @@ class SearchActivity : AppCompatActivity() {
|
||||
|
||||
progressAdapter = ProgressAdapter(searched = model.searched)
|
||||
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
|
||||
val headerAdaptor = SearchAdapter(this)
|
||||
headerAdaptor = SearchAdapter(this, model.searchResults.type)
|
||||
|
||||
val gridSize = (screenWidth / 124f).toInt()
|
||||
val gridSize = (screenWidth / 120f).toInt()
|
||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (position) {
|
||||
0 -> gridSize
|
||||
0 -> gridSize
|
||||
concatAdapter.itemCount - 1 -> gridSize
|
||||
else -> when (style) {
|
||||
0 -> 1
|
||||
else -> when (style) {
|
||||
0 -> 1
|
||||
else -> gridSize
|
||||
}
|
||||
}
|
||||
@@ -147,17 +151,26 @@ class SearchActivity : AppCompatActivity() {
|
||||
} else
|
||||
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 loading = false
|
||||
fun search() {
|
||||
headerAdaptor.setHistoryVisibility(false)
|
||||
val size = model.searchResults.results.size
|
||||
model.searchResults.results.clear()
|
||||
runOnUiThread {
|
||||
binding.searchRecyclerView.post {
|
||||
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
@@ -186,6 +199,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
|
||||
var state: Parcelable? = null
|
||||
override fun onPause() {
|
||||
headerAdaptor.addHistory()
|
||||
super.onPause()
|
||||
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
@@ -1,48 +1,71 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||
import ani.dantotsu.App.Companion.context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.android.material.checkbox.MaterialCheckBox.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
||||
class SearchAdapter(private val activity: SearchActivity, private val type: String) :
|
||||
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
||||
private val itemViewType = 6969
|
||||
var search: Runnable? = null
|
||||
var requestFocus: Runnable? = null
|
||||
private var textWatcher: TextWatcher? = null
|
||||
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
||||
private lateinit var binding: ItemSearchHeaderBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||
val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SearchHeaderViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
binding = holder.binding
|
||||
|
||||
searchHistoryAdapter = SearchHistoryAdapter(type) {
|
||||
binding.searchBarText.setText(it)
|
||||
}
|
||||
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
|
||||
binding.searchHistoryList.adapter = searchHistoryAdapter
|
||||
|
||||
val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val imm: InputMethodManager =
|
||||
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
when (activity.style) {
|
||||
0 -> {
|
||||
binding.searchResultGrid.alpha = 1f
|
||||
binding.searchResultList.alpha = 0.33f
|
||||
}
|
||||
|
||||
1 -> {
|
||||
binding.searchResultList.alpha = 1f
|
||||
binding.searchResultGrid.alpha = 0.33f
|
||||
@@ -50,6 +73,13 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
}
|
||||
|
||||
binding.searchBar.hint = activity.result.type
|
||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||
val startIconDrawableRes = R.drawable.ic_incognito_24
|
||||
val startIconDrawable: Drawable? =
|
||||
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
|
||||
binding.searchBar.startIconDrawable = startIconDrawable
|
||||
}
|
||||
|
||||
var adult = activity.result.isAdult
|
||||
var listOnly = activity.result.onList
|
||||
|
||||
@@ -62,7 +92,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
||||
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 {
|
||||
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
||||
@@ -70,10 +101,14 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
|
||||
fun searchTitle() {
|
||||
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
|
||||
isAdult = adult
|
||||
}
|
||||
if (binding.searchBarText.text.toString().equals("hentai", true)) {
|
||||
openLinkInBrowser("https://www.youtube.com/watch?v=GgJrEOo0QoA")
|
||||
}
|
||||
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 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)
|
||||
@@ -96,7 +142,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
binding.searchBar.setEndIconOnClickListener { searchTitle() }
|
||||
@@ -105,14 +152,14 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
it.alpha = 1f
|
||||
binding.searchResultList.alpha = 0.33f
|
||||
activity.style = 0
|
||||
saveData("searchStyle", 0)
|
||||
PrefManager.setVal(PrefName.SearchStyle, 0)
|
||||
activity.recycler()
|
||||
}
|
||||
binding.searchResultList.setOnClickListener {
|
||||
it.alpha = 1f
|
||||
binding.searchResultGrid.alpha = 0.33f
|
||||
activity.style = 1
|
||||
saveData("searchStyle", 1)
|
||||
PrefManager.setVal(PrefName.SearchStyle, 1)
|
||||
activity.recycler()
|
||||
}
|
||||
|
||||
@@ -127,7 +174,7 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
binding.searchList.apply {
|
||||
if (Anilist.userid != null) {
|
||||
visibility = View.VISIBLE
|
||||
checkedState = when(listOnly){
|
||||
checkedState = when (listOnly) {
|
||||
null -> STATE_UNCHECKED
|
||||
true -> STATE_CHECKED
|
||||
false -> STATE_INDETERMINATE
|
||||
@@ -135,10 +182,10 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
|
||||
addOnCheckedStateChangedListener { _, state ->
|
||||
listOnly = when (state) {
|
||||
STATE_CHECKED -> true
|
||||
STATE_CHECKED -> true
|
||||
STATE_INDETERMINATE -> false
|
||||
STATE_UNCHECKED -> null
|
||||
else -> null
|
||||
STATE_UNCHECKED -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,23 +202,61 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
|
||||
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
|
||||
|
||||
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 {
|
||||
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()
|
||||
|
||||
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 {
|
||||
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SearchChipViewHolder(binding)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,14 +17,19 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import com.google.android.material.chip.Chip
|
||||
import java.util.Calendar
|
||||
|
||||
class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
||||
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSearchFilterBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
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)
|
||||
return binding.root
|
||||
}
|
||||
@@ -99,7 +104,8 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
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.searchFilterTags.adapter = FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
||||
val tag = chip.text.toString()
|
||||
chip.isChecked = selectedTags.contains(tag)
|
||||
chip.isCloseIconVisible = exTags.contains(tag)
|
||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
chip.isCloseIconVisible = false
|
||||
exTags.remove(tag)
|
||||
selectedTags.add(tag)
|
||||
} else
|
||||
selectedTags.remove(tag)
|
||||
binding.searchFilterTags.adapter =
|
||||
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
||||
val tag = chip.text.toString()
|
||||
chip.isChecked = selectedTags.contains(tag)
|
||||
chip.isCloseIconVisible = exTags.contains(tag)
|
||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
chip.isCloseIconVisible = false
|
||||
exTags.remove(tag)
|
||||
selectedTags.add(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.searchFilterTags.layoutManager =
|
||||
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)) :
|
||||
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 {
|
||||
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding =
|
||||
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,10 @@ data class Selected(
|
||||
var chip: Int = 0,
|
||||
//var source: String = "",
|
||||
var sourceIndex: Int = 0,
|
||||
var langIndex: Int = 0,
|
||||
var preferDub: Boolean = false,
|
||||
var server: String? = null,
|
||||
var video: Int = 0,
|
||||
var latest: Float = 0f,
|
||||
var scanlators: List<String>? = null,
|
||||
) : Serializable
|
||||
|
||||
@@ -17,7 +17,8 @@ abstract class SourceAdapter(
|
||||
private val scope: CoroutineScope
|
||||
) : RecyclerView.Adapter<SourceAdapter.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)
|
||||
}
|
||||
|
||||
@@ -34,7 +35,8 @@ abstract class SourceAdapter(
|
||||
|
||||
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 {
|
||||
itemView.setOnClickListener {
|
||||
dialogFragment.dismiss()
|
||||
|
||||
@@ -13,8 +13,8 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.media.anime.AnimeSourceAdapter
|
||||
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
|
||||
import ani.dantotsu.media.anime.AnimeSourceAdapter
|
||||
import ani.dantotsu.media.manga.MangaSourceAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
@@ -38,7 +38,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
var id: Int? = 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)
|
||||
return binding.root
|
||||
}
|
||||
@@ -47,7 +51,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
|
||||
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) {
|
||||
media = it
|
||||
if (media != null) {
|
||||
@@ -65,6 +70,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
anime = false
|
||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||
}
|
||||
|
||||
fun search() {
|
||||
binding.searchBarText.clearFocus()
|
||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||
@@ -86,7 +92,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
search()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
binding.searchBar.setEndIconOnClickListener { search() }
|
||||
@@ -101,7 +108,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
|
||||
binding.searchRecyclerView.layoutManager = GridLayoutManager(
|
||||
requireActivity(),
|
||||
clamp(requireActivity().resources.displayMetrics.widthPixels / 124f.px, 1, 4)
|
||||
clamp(
|
||||
requireActivity().resources.displayMetrics.widthPixels / 124f.px,
|
||||
1,
|
||||
4
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,15 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
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.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -29,6 +35,7 @@ class StudioActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityStudioBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
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.snackString
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
|
||||
class SubtitleDownloader {
|
||||
|
||||
companion object {
|
||||
//doesn't really download the subtitles -\_(o_o)_/-
|
||||
suspend fun downloadSubtitles(context: Context, url: String): SubtitleType =
|
||||
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
|
||||
withContext(Dispatchers.IO) {
|
||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
@@ -30,12 +29,12 @@ class SubtitleDownloader {
|
||||
|
||||
// Check if response is successful
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body?.string()
|
||||
val responseBody = response.body.string()
|
||||
|
||||
|
||||
val subtitleType = when {
|
||||
responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS
|
||||
responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT
|
||||
responseBody.contains("[Script Info]") -> SubtitleType.ASS
|
||||
responseBody.contains("WEBVTT") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
}
|
||||
|
||||
@@ -44,5 +43,50 @@ class SubtitleDownloader {
|
||||
return@withContext SubtitleType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
//actually downloads lol
|
||||
suspend fun downloadSubtitle(
|
||||
context: Context,
|
||||
url: String,
|
||||
downloadedType: DownloadedType
|
||||
) {
|
||||
try {
|
||||
val directory = DownloadsManager.getDirectory(
|
||||
context,
|
||||
downloadedType.type,
|
||||
downloadedType.title,
|
||||
downloadedType.chapter
|
||||
)
|
||||
if (!directory.exists()) { //just in case
|
||||
directory.mkdirs()
|
||||
}
|
||||
val type = loadSubtitleType(context, url)
|
||||
val subtiteFile = File(directory, "subtitle.${type}")
|
||||
if (subtiteFile.exists()) {
|
||||
subtiteFile.delete()
|
||||
}
|
||||
subtiteFile.createNewFile()
|
||||
|
||||
val client = Injekt.get<NetworkHelper>().client
|
||||
val request = Request.Builder().url(url).build()
|
||||
val reponse = client.newCall(request).execute()
|
||||
|
||||
if (!reponse.isSuccessful) {
|
||||
snackString("Failed to download subtitle")
|
||||
return
|
||||
}
|
||||
|
||||
reponse.body.byteStream().use { input ->
|
||||
subtiteFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to download subtitle")
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.databinding.ItemTitleBinding
|
||||
|
||||
class TitleAdapter(private val text: String) : RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
|
||||
inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
class TitleAdapter(private val text: String) :
|
||||
RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
|
||||
inner class TitleViewHolder(val binding: ItemTitleBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
|
||||
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
|
||||
@@ -15,7 +15,7 @@ data class Anime(
|
||||
var ed: ArrayList<String> = arrayListOf(),
|
||||
|
||||
var mainStudio: Studio? = null,
|
||||
var author: Author?=null,
|
||||
var author: Author? = null,
|
||||
|
||||
var youtube: String? = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,28 +3,40 @@ package ani.dantotsu.media.anime
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.others.webview.CookieCatcher
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class AnimeWatchAdapter(
|
||||
private val media: Media,
|
||||
private val fragment: AnimeWatchFragment,
|
||||
@@ -39,13 +51,16 @@ class AnimeWatchAdapter(
|
||||
return ViewHolder(bind)
|
||||
}
|
||||
|
||||
private var nestedDialog: AlertDialog? = null
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
_binding = binding
|
||||
|
||||
//Youtube
|
||||
if (media.anime!!.youtube != null && fragment.uiSettings.showYtButton) {
|
||||
if (media.anime!!.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
|
||||
binding.animeSourceYT.visibility = View.VISIBLE
|
||||
binding.animeSourceYT.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
|
||||
@@ -54,22 +69,42 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
|
||||
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
|
||||
var changing = false
|
||||
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)
|
||||
}
|
||||
|
||||
//Wrong Title
|
||||
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
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||
var source =
|
||||
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||
setLanguageList(media.selected!!.langIndex, source)
|
||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||
binding.animeSource.setText(watchSources.names[source])
|
||||
watchSources[source].apply {
|
||||
@@ -77,11 +112,17 @@ class AnimeWatchAdapter(
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
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.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||
fragment.onSourceChange(i).apply {
|
||||
@@ -90,14 +131,48 @@ class AnimeWatchAdapter(
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
source = i
|
||||
setLanguageList(0, i)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadEpisodes(i)
|
||||
fragment.loadEpisodes(i, false)
|
||||
}
|
||||
|
||||
//Subscription
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
ext.sourceLanguage = i
|
||||
fragment.onLangChange(i)
|
||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
setLanguageList(i, source)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
} ?: run {
|
||||
}
|
||||
}
|
||||
|
||||
//settings
|
||||
binding.animeSourceSettings.setOnClickListener {
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
fragment.openSettings(ext.extension)
|
||||
}
|
||||
}
|
||||
|
||||
//Icons
|
||||
|
||||
//subscribe
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
binding.animeSourceSubscribe,
|
||||
R.drawable.ic_round_notifications_active_24,
|
||||
@@ -112,51 +187,115 @@ class AnimeWatchAdapter(
|
||||
subscribeButton(false)
|
||||
|
||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||
openSettings(fragment.requireContext(),getChannelId(true,media.id))
|
||||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||
}
|
||||
|
||||
//Icons
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
binding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
var selected = when (style) {
|
||||
0 -> binding.animeSourceList
|
||||
1 -> binding.animeSourceGrid
|
||||
2 -> binding.animeSourceCompact
|
||||
else -> binding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
fun selected(it: ImageView) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
//Nested Button
|
||||
binding.animeNestedButton.setOnClickListener {
|
||||
val dialogView =
|
||||
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||
var refresh = false
|
||||
var run = false
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style =
|
||||
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
dialogBinding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
run = true
|
||||
}
|
||||
//Grids
|
||||
var selected = when (style) {
|
||||
0 -> dialogBinding.animeSourceList
|
||||
1 -> dialogBinding.animeSourceGrid
|
||||
2 -> dialogBinding.animeSourceCompact
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> dialogBinding.layoutText.text = "List"
|
||||
1 -> dialogBinding.layoutText.text = "Grid"
|
||||
2 -> dialogBinding.layoutText.text = "Compact"
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
}
|
||||
binding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeSourceGrid.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
binding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 2
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
fun selected(it: ImageButton) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
dialogBinding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 0
|
||||
dialogBinding.layoutText.text = "List"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceGrid.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 1
|
||||
dialogBinding.layoutText.text = "Grid"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 2
|
||||
dialogBinding.layoutText.text = "Compact"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||
toast("WebView not installed")
|
||||
}
|
||||
//start CookieCatcher activity
|
||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||
val sourceAHH = watchSources[source] as? DynamicAnimeParser
|
||||
val sourceHttp =
|
||||
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
|
||||
val url = sourceHttp?.baseUrl
|
||||
url?.let {
|
||||
refresh = true
|
||||
val headersMap = try {
|
||||
sourceHttp.headers.toMultimap()
|
||||
.mapValues { it.value.getOrNull(0) ?: "" }
|
||||
} catch (e: Exception) {
|
||||
emptyMap()
|
||||
}
|
||||
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
.putExtra("headers", headersMap as HashMap<String, String>)
|
||||
startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//hidden
|
||||
dialogBinding.animeScanlatorContainer.visibility = View.GONE
|
||||
dialogBinding.animeDownloadContainer.visibility = View.GONE
|
||||
|
||||
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
|
||||
.setTitle("Options")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { _, _ ->
|
||||
if (run) fragment.onIconPressed(style, reversed)
|
||||
if (refresh) fragment.loadEpisodes(source, true)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ ->
|
||||
if (refresh) fragment.loadEpisodes(source, true)
|
||||
}
|
||||
.setOnCancelListener {
|
||||
if (refresh) fragment.loadEpisodes(source, true)
|
||||
}
|
||||
.create()
|
||||
nestedDialog?.show()
|
||||
}
|
||||
//Episode Handling
|
||||
handleEpisodes()
|
||||
}
|
||||
|
||||
fun subscribeButton(enabled : Boolean) {
|
||||
fun subscribeButton(enabled: Boolean) {
|
||||
subscribe?.enabled(enabled)
|
||||
}
|
||||
|
||||
@@ -170,13 +309,26 @@ class AnimeWatchAdapter(
|
||||
for (position in arr.indices) {
|
||||
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
|
||||
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
|
||||
fun selected() {
|
||||
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.setTextColor(
|
||||
ContextCompat.getColorStateList(
|
||||
fragment.requireContext(),
|
||||
R.color.chip_text_color
|
||||
)
|
||||
)
|
||||
|
||||
chip.setOnClickListener {
|
||||
selected()
|
||||
@@ -189,7 +341,14 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +364,9 @@ class AnimeWatchAdapter(
|
||||
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
||||
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp = loadData<String>("${media.id}_current_ep")?.toIntOrNull() ?: 1
|
||||
val appEp =
|
||||
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
|
||||
?: 1
|
||||
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
if (episodes.contains(continueEp)) {
|
||||
@@ -217,7 +378,10 @@ class AnimeWatchAdapter(
|
||||
media.id,
|
||||
continueEp
|
||||
)
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > fragment.playerSettings.watchPercentage) {
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > PrefManager.getVal<Float>(
|
||||
PrefName.WatchPercentage
|
||||
)
|
||||
) {
|
||||
val e = episodes.indexOf(continueEp)
|
||||
if (e != -1 && e + 1 < episodes.size) {
|
||||
continueEp = episodes[e + 1]
|
||||
@@ -231,15 +395,23 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
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
|
||||
binding.animeSourceContinueText.text =
|
||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
|
||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
|
||||
binding.animeSourceContinue.setOnClickListener {
|
||||
fragment.onEpisodeClick(continueEp)
|
||||
}
|
||||
if (fragment.continueEp) {
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < fragment.playerSettings.watchPercentage) {
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
|
||||
PrefName.WatchPercentage
|
||||
)
|
||||
) {
|
||||
binding.animeSourceContinue.performClick()
|
||||
fragment.continueEp = false
|
||||
}
|
||||
@@ -247,6 +419,7 @@ class AnimeWatchAdapter(
|
||||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.animeSourceProgressBar.visibility = View.GONE
|
||||
if (media.anime.episodes!!.isNotEmpty())
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
@@ -261,12 +434,43 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (watchSources is AnimeSources) {
|
||||
val parser = watchSources[source] as? DynamicAnimeParser
|
||||
if (parser != null) {
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
ext.sourceLanguage = lang
|
||||
}
|
||||
try {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
binding?.animeSourceLanguage?.setText(
|
||||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
val adapter = ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||
)
|
||||
val items = adapter.count
|
||||
|
||||
binding?.animeSourceLanguageContainer?.visibility =
|
||||
if (items > 1) View.VISIBLE else View.GONE
|
||||
binding?.animeSourceLanguage?.setAdapter(adapter)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
//Timer
|
||||
countDown(media, binding.animeSourceContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user