mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-27 07:51:02 +00:00
Compare commits
205 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
870cb751a4 | ||
|
|
4ffe9d7505 | ||
|
|
513b937e59 | ||
|
|
6113a10556 | ||
|
|
233f4bfb48 | ||
|
|
3fd01d582a | ||
|
|
00758af458 | ||
|
|
4477e3a0e1 | ||
|
|
e475cc5c01 | ||
|
|
3622d91886 | ||
|
|
3c46c21a25 | ||
|
|
44178b2de2 | ||
|
|
13f5d0978d | ||
|
|
70a50ece43 | ||
|
|
24147e746a | ||
|
|
386e02a564 | ||
|
|
865b96a219 | ||
|
|
dd38bb156b | ||
|
|
72c07b7d7a | ||
|
|
3f19cadffc | ||
|
|
670d16bd8e | ||
|
|
3d1040b280 | ||
|
|
cd3bb20afd | ||
|
|
91d1d2cf1d | ||
|
|
f8a6fad513 | ||
|
|
9d3d394c7d | ||
|
|
820a09b28f | ||
|
|
108285021e | ||
|
|
1d005585c8 | ||
|
|
714591dd2e | ||
|
|
6e399b32e1 | ||
|
|
4b413b78fe | ||
|
|
126bc6134e | ||
|
|
bf33f5d9c8 | ||
|
|
a8ff4fdc26 | ||
|
|
7ca44480a9 | ||
|
|
ea29449413 | ||
|
|
9ec448e503 | ||
|
|
70be4e92fb | ||
|
|
c0e3243ee6 | ||
|
|
b961701189 | ||
|
|
3619355cb4 | ||
|
|
674a512630 | ||
|
|
5e5277404e | ||
|
|
c242d9dd99 | ||
|
|
a5a94e5003 | ||
|
|
9b6dc1318d | ||
|
|
87535a9239 | ||
|
|
6be589618c | ||
|
|
a51e025c03 | ||
|
|
29e115ce41 | ||
|
|
f96d2ffaa5 | ||
|
|
47d05e737d | ||
|
|
3666758e6e | ||
|
|
e49f0dbf32 | ||
|
|
abe3f883ae | ||
|
|
e5cb7c7fdf | ||
|
|
79337b5e7f | ||
|
|
ae5907e6b3 | ||
|
|
04fb31eff9 | ||
|
|
9f7e01a1fb | ||
|
|
9ace8e5235 | ||
|
|
771cdcc163 | ||
|
|
58d5b5bc41 | ||
|
|
04538c52f2 | ||
|
|
dd994dcfab | ||
|
|
594b71dc16 | ||
|
|
cf7ccaebd1 | ||
|
|
8bde831794 | ||
|
|
2f30bdb6a8 | ||
|
|
4d28ae2e3e | ||
|
|
5fcbfeb3db | ||
|
|
f6c7b09d9b | ||
|
|
72c69e7c79 | ||
|
|
13a65c2bfa | ||
|
|
d2f118a86c | ||
|
|
ce11c71e95 | ||
|
|
e4574d6c03 | ||
|
|
d8c311fbd7 | ||
|
|
d6e6c6f8fb | ||
|
|
63c3058f5b | ||
|
|
0d5815d3c9 | ||
|
|
dec4996760 | ||
|
|
e0df092a70 | ||
|
|
da56aecd5e | ||
|
|
7688ffa39f | ||
|
|
d08e89bb63 | ||
|
|
5979479619 | ||
|
|
e1b968bfe0 | ||
|
|
36c476bc36 | ||
|
|
6bfadfa962 | ||
|
|
720b40afa7 | ||
|
|
75e90541c9 | ||
|
|
47b1940ace | ||
|
|
012b1cd79d | ||
|
|
ff3131d988 | ||
|
|
ba1725224a | ||
|
|
55bc2add85 | ||
|
|
9e96fd1e20 | ||
|
|
79d20b0b63 | ||
|
|
b2a44cfe09 | ||
|
|
146805af49 | ||
|
|
aabbe9198a | ||
|
|
a815bac15d | ||
|
|
86427a4c3c | ||
|
|
0d8a82568a | ||
|
|
95b2939532 | ||
|
|
76e11e5a3e | ||
|
|
2d5d02fd67 | ||
|
|
f30e6b7809 | ||
|
|
04f2034dd1 | ||
|
|
99b3bbaaad | ||
|
|
c0bccc027f | ||
|
|
51beac2d03 | ||
|
|
63a5150cea | ||
|
|
e34a20bce6 | ||
|
|
ca482ea9d4 | ||
|
|
e31d2ada4f | ||
|
|
c29147a681 | ||
|
|
92be9bf626 | ||
|
|
a02b8b7b0a | ||
|
|
1c1d14fff1 | ||
|
|
eff0a34c54 | ||
|
|
2dc3035a7c | ||
|
|
78f6ec27b3 | ||
|
|
6b868fa824 | ||
|
|
7951c2cf37 | ||
|
|
ea678ef55e | ||
|
|
fbbbf41595 | ||
|
|
f83d1d8d84 | ||
|
|
7bcc01b94e | ||
|
|
ff72f9dbdf | ||
|
|
b1210570d1 | ||
|
|
ef97b5679e | ||
|
|
6dfe0269bf | ||
|
|
77c57846ed | ||
|
|
19b5b11b07 | ||
|
|
27d4ce3c5b | ||
|
|
859aa01ec2 | ||
|
|
6d102f7be3 | ||
|
|
5ae1ead2c9 | ||
|
|
b1982013dc | ||
|
|
954fdde1c4 | ||
|
|
f177e2cf7c | ||
|
|
845ebb4868 | ||
|
|
b43171bb31 | ||
|
|
be07fad8f1 | ||
|
|
3375496ef2 | ||
|
|
df23b2f62f | ||
|
|
95cddbd409 | ||
|
|
d46f1b25eb | ||
|
|
378abe73c9 | ||
|
|
b5eda797b5 | ||
|
|
f704e322af | ||
|
|
dc21d28b83 | ||
|
|
eb17862177 | ||
|
|
fc023f307a | ||
|
|
ad1905c8fe | ||
|
|
85d54e8f5e | ||
|
|
ba09f7533c | ||
|
|
fa6e3a34b5 | ||
|
|
85ef4b3c12 | ||
|
|
89e18b0e2f | ||
|
|
1b50ffcf11 | ||
|
|
b3f83816c5 | ||
|
|
75b78886ae | ||
|
|
26d97da066 | ||
|
|
ab7bc15573 | ||
|
|
d43d643bbd | ||
|
|
3ca5efc177 | ||
|
|
04c858e6fd | ||
|
|
25046e4c11 | ||
|
|
5134776e2f | ||
|
|
cc29ebd75b | ||
|
|
2233f1ce44 | ||
|
|
a189802061 | ||
|
|
dca6ffdbbe | ||
|
|
859946a751 | ||
|
|
08bf1a2336 | ||
|
|
27743e3427 | ||
|
|
52b0cc4129 | ||
|
|
22abc2e21d | ||
|
|
fc8425b12a | ||
|
|
60fc1fa74b | ||
|
|
190e3ce7bb | ||
|
|
012024ab77 | ||
|
|
529bdd74c8 | ||
|
|
6e349b84c0 | ||
|
|
ab9b92035e | ||
|
|
37ec165319 | ||
|
|
958aa634b1 | ||
|
|
125a95285d | ||
|
|
bbaae2e776 | ||
|
|
f9090f59b7 | ||
|
|
1d740d33a0 | ||
|
|
633ec19c90 | ||
|
|
9b2015f4cf | ||
|
|
e65e7a79a5 | ||
|
|
0996639cac | ||
|
|
e5f58f20c7 | ||
|
|
d1e03b8237 | ||
|
|
917ffe644f | ||
|
|
02efc01a10 | ||
|
|
3016792f95 | ||
|
|
e1b50c86f3 |
25
.github/workflows/beta.yml
vendored
25
.github/workflows/beta.yml
vendored
@@ -48,9 +48,11 @@ jobs:
|
|||||||
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
|
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
|
||||||
# Debugging: Print the variable to check its content
|
# Debugging: Print the variable to check its content
|
||||||
echo "$COMMIT_LOGS"
|
echo "$COMMIT_LOGS"
|
||||||
|
echo "$COMMIT_LOGS" > commit_log.txt
|
||||||
shell: /usr/bin/bash -e {0}
|
shell: /usr/bin/bash -e {0}
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Save Current SHA for Next Run
|
- name: Save Current SHA for Next Run
|
||||||
run: echo ${{ github.sha }} > last_sha.txt
|
run: echo ${{ github.sha }} > last_sha.txt
|
||||||
@@ -75,7 +77,7 @@ jobs:
|
|||||||
|
|
||||||
- name: List files in the directory
|
- name: List files in the directory
|
||||||
run: ls -l
|
run: ls -l
|
||||||
|
|
||||||
- name: Make gradlew executable
|
- name: Make gradlew executable
|
||||||
run: chmod +x ./gradlew
|
run: chmod +x ./gradlew
|
||||||
|
|
||||||
@@ -83,9 +85,11 @@ jobs:
|
|||||||
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 }}
|
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
|
- name: Upload a Build Artifact
|
||||||
uses: actions/upload-artifact@v4.3.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Dantotsu
|
name: Dantotsu
|
||||||
|
retention-days: 5
|
||||||
|
compression-level: 9
|
||||||
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
||||||
|
|
||||||
- name: Upload APK to Discord and Telegram
|
- name: Upload APK to Discord and Telegram
|
||||||
@@ -99,7 +103,7 @@ jobs:
|
|||||||
if [ ${#commit_messages} -gt $max_length ]; then
|
if [ ${#commit_messages} -gt $max_length ]; then
|
||||||
commit_messages="${commit_messages:0:$max_length}... (truncated)"
|
commit_messages="${commit_messages:0:$max_length}... (truncated)"
|
||||||
fi
|
fi
|
||||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$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 }}
|
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
#Telegram
|
#Telegram
|
||||||
@@ -113,18 +117,13 @@ jobs:
|
|||||||
VERSION: ${{ env.VERSION }}
|
VERSION: ${{ env.VERSION }}
|
||||||
|
|
||||||
- name: Upload Current SHA as Artifact
|
- name: Upload Current SHA as Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: last-sha
|
name: last-sha
|
||||||
path: last_sha.txt
|
path: last_sha.txt
|
||||||
|
|
||||||
|
- name: Upload Commit log as Artifact
|
||||||
- name: Delete Old Pre-Releases
|
uses: actions/upload-artifact@v4
|
||||||
id: delete-pre-releases
|
|
||||||
uses: sgpublic/delete-release-action@master
|
|
||||||
with:
|
with:
|
||||||
pre-release-drop: true
|
name: commit-log
|
||||||
pre-release-keep-count: 3
|
path: commit_log.txt
|
||||||
pre-release-drop-tag: true
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,3 +31,6 @@ output.json
|
|||||||
|
|
||||||
#other
|
#other
|
||||||
scripts/
|
scripts/
|
||||||
|
|
||||||
|
#crowdin
|
||||||
|
crowdin.yml
|
||||||
@@ -6,6 +6,10 @@ plugins {
|
|||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def gitCommitHash = providers.exec {
|
||||||
|
commandLine("git", "rev-parse", "--verify", "--short", "HEAD")
|
||||||
|
}.standardOutput.asText.get().trim()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
|
|
||||||
@@ -17,6 +21,7 @@ android {
|
|||||||
versionName "3.0.0"
|
versionName "3.0.0"
|
||||||
versionCode 300000000
|
versionCode 300000000
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions += "store"
|
flavorDimensions += "store"
|
||||||
@@ -38,7 +43,7 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
alpha {
|
alpha {
|
||||||
applicationIdSuffix ".beta" // keep as beta by popular request
|
applicationIdSuffix ".beta" // keep as beta by popular request
|
||||||
versionNameSuffix "-alpha01"
|
versionNameSuffix "-alpha01-" + gitCommitHash
|
||||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
|
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
|
||||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
|
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
|
||||||
debuggable System.getenv("CI") == null
|
debuggable System.getenv("CI") == null
|
||||||
@@ -46,7 +51,7 @@ android {
|
|||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix ".beta"
|
applicationIdSuffix ".beta"
|
||||||
versionNameSuffix "-beta01"
|
versionNameSuffix "-beta02"
|
||||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
|
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
|
||||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
|
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
|
||||||
debuggable false
|
debuggable false
|
||||||
@@ -75,11 +80,11 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// FireBase
|
// FireBase
|
||||||
googleImplementation platform('com.google.firebase:firebase-bom:32.7.4')
|
googleImplementation platform('com.google.firebase:firebase-bom:32.8.1')
|
||||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
|
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.6.2'
|
||||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
|
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.4'
|
||||||
// Core
|
// Core
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.browser:browser:1.8.0'
|
implementation 'androidx.browser:browser:1.8.0'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
@@ -95,8 +100,9 @@ dependencies {
|
|||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.webkit:webkit:1.10.0'
|
implementation 'androidx.webkit:webkit:1.10.0'
|
||||||
|
implementation "com.anggrayudi:storage:1.5.5"
|
||||||
|
|
||||||
// Glide
|
// Glide
|
||||||
ext.glide_version = '4.16.0'
|
ext.glide_version = '4.16.0'
|
||||||
api "com.github.bumptech.glide:glide:$glide_version"
|
api "com.github.bumptech.glide:glide:$glide_version"
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
@@ -104,49 +110,48 @@ dependencies {
|
|||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
|
|
||||||
// Exoplayer
|
// Exoplayer
|
||||||
ext.exo_version = '1.3.0'
|
ext.exo_version = '1.3.1'
|
||||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||||
implementation "androidx.media3:media3-ui:$exo_version"
|
implementation "androidx.media3:media3-ui:$exo_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||||
implementation "androidx.media3:media3-session:$exo_version"
|
implementation "androidx.media3:media3-session:$exo_version"
|
||||||
//media3 casting
|
// Media3 Casting
|
||||||
implementation "androidx.media3:media3-cast:$exo_version"
|
implementation "androidx.media3:media3-cast:$exo_version"
|
||||||
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
implementation "androidx.mediarouter:mediarouter:1.7.0"
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
//implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
implementation 'com.github.RepoDevil:AnimatedBottomBar:7fcb9af'
|
||||||
implementation 'com.github.rebelonion:AnimatedBottomBar:v1.1.0'
|
|
||||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc'
|
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
|
||||||
|
|
||||||
// Markwon
|
// Markwon
|
||||||
ext.markwon_version = '4.6.2'
|
ext.markwon_version = '4.6.2'
|
||||||
implementation "io.noties.markwon:core:$markwon_version"
|
implementation "io.noties.markwon:core:$markwon_version"
|
||||||
implementation "io.noties.markwon:editor:$markwon_version"
|
implementation "io.noties.markwon:editor:$markwon_version"
|
||||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||||
implementation "io.noties.markwon:ext-tables:$markwon_version"
|
implementation "io.noties.markwon:ext-tables:$markwon_version"
|
||||||
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
|
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
|
||||||
implementation "io.noties.markwon:html:$markwon_version"
|
implementation "io.noties.markwon:html:$markwon_version"
|
||||||
implementation "io.noties.markwon:image-glide:$markwon_version"
|
implementation "io.noties.markwon:image-glide:$markwon_version"
|
||||||
|
|
||||||
// Groupie
|
// Groupie
|
||||||
ext.groupie_version = '2.10.1'
|
ext.groupie_version = '2.10.1'
|
||||||
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
|
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
|
||||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
|
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
|
||||||
|
|
||||||
// string matching
|
// String Matching
|
||||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||||
|
|
||||||
// Aniyomi
|
// Aniyomi
|
||||||
implementation 'io.reactivex:rxjava:1.3.8'
|
implementation 'io.reactivex:rxjava:1.3.8'
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
|
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import com.google.firebase.FirebaseApp
|
|||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import com.google.firebase.ktx.app
|
|
||||||
|
|
||||||
class FirebaseCrashlytics : CrashlyticsInterface {
|
class FirebaseCrashlytics : CrashlyticsInterface {
|
||||||
override fun initialize(context: Context) {
|
override fun initialize(context: Context) {
|
||||||
FirebaseApp.initializeApp(context)
|
FirebaseApp.initializeApp(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logException(e: Throwable) {
|
override fun logException(e: Throwable) {
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,13 +85,18 @@ object AppUpdater {
|
|||||||
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||||
MainScope().launch(Dispatchers.IO) {
|
MainScope().launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
val apks =
|
||||||
.parsed<GithubResponse>().assets?.find {
|
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||||
it.browserDownloadURL.endsWith("apk")
|
.parsed<GithubResponse>().assets?.filter {
|
||||||
}?.browserDownloadURL.apply {
|
it.browserDownloadURL.endsWith(
|
||||||
if (this != null) activity.downloadUpdate(version, this)
|
".apk"
|
||||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
)
|
||||||
}
|
}
|
||||||
|
val apkToDownload = apks?.first()
|
||||||
|
apkToDownload?.browserDownloadURL.apply {
|
||||||
|
if (this != null) activity.downloadUpdate(version, this)
|
||||||
|
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
@@ -111,24 +116,25 @@ object AppUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compareVersion(version: String): Boolean {
|
private fun compareVersion(version: String): Boolean {
|
||||||
|
return when (BuildConfig.BUILD_TYPE) {
|
||||||
|
"debug" -> BuildConfig.VERSION_NAME != version
|
||||||
|
"alpha" -> false
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}.sum()
|
||||||
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
val new = toDouble(version.split("."))
|
||||||
return BuildConfig.VERSION_NAME != version
|
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
||||||
} else {
|
new > curr
|
||||||
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
|
|
||||||
}
|
|
||||||
}.sum()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val new = toDouble(version.split("."))
|
|
||||||
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
|
||||||
return new > curr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-sdk tools:overrideLibrary="go.server.gojni" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.software.leanback"
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="29" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||||
@@ -38,6 +40,17 @@
|
|||||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
|
<!-- ExoPlayer: Bluetooth Headsets -->
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.bluetooth"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<!-- ExoPlayer: Bluetooth Headsets -->
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<package android:name="idm.internet.download.manager.plus" />
|
<package android:name="idm.internet.download.manager.plus" />
|
||||||
<package android:name="idm.internet.download.manager" />
|
<package android:name="idm.internet.download.manager" />
|
||||||
@@ -49,6 +62,7 @@
|
|||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@mipmap/ic_banner_foreground"
|
android:banner="@mipmap/ic_banner_foreground"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:icon="${icon_placeholder}"
|
android:icon="${icon_placeholder}"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
@@ -57,9 +71,30 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Dantotsu"
|
android:theme="@style/Theme.Dantotsu"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="AllowBackup">
|
tools:ignore="AllowBackup"
|
||||||
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widgets.CurrentlyAiringWidget"
|
android:name=".widgets.upcoming.UpcomingWidget"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/upcoming_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
<activity
|
||||||
|
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widgets.statistics.ProfileStatsWidget"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
@@ -67,11 +102,10 @@
|
|||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/currently_airing_widget_info" />
|
android:resource="@xml/statistics_widget_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
|
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".media.novel.novelreader.NovelReaderActivity"
|
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
@@ -103,27 +137,61 @@
|
|||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.ExtensionsActivity"
|
android:name=".settings.SettingsAboutActivity"
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden"
|
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsAccountActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsAnimeActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsCommonActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsExtensionsActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsAddonActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsMangaActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsNotificationActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsThemeActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".settings.ExtensionsActivity"
|
||||||
|
android:parentActivityName=".MainActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
|
<activity
|
||||||
|
android:name=".widgets.statistics.ProfileStatsConfigure"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".profile.ProfileActivity"
|
android:name=".profile.ProfileActivity"
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden"
|
android:parentActivityName=".MainActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".profile.FollowActivity"
|
android:name=".profile.FollowActivity"
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden"
|
android:parentActivityName=".MainActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".profile.activity.FeedActivity"
|
android:name=".profile.activity.FeedActivity"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout"
|
||||||
android:label="Inbox Activity"
|
android:label="Inbox Activity"
|
||||||
android:parentActivityName=".MainActivity" >
|
android:parentActivityName=".MainActivity" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".profile.activity.NotificationActivity"
|
android:name=".profile.activity.NotificationActivity"
|
||||||
android:label="Inbox Activity"
|
android:label="Inbox Activity"
|
||||||
android:parentActivityName=".MainActivity" >
|
android:parentActivityName=".MainActivity" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".others.imagesearch.ImageSearchActivity"
|
android:name=".others.imagesearch.ImageSearchActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
@@ -136,8 +204,9 @@
|
|||||||
android:name=".media.CalendarActivity"
|
android:name=".media.CalendarActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
<activity android:name=".media.user.ListActivity" />
|
<activity android:name=".media.user.ListActivity" />
|
||||||
<activity android:name=".profile.SingleStatActivity"
|
<activity
|
||||||
android:parentActivityName=".profile.ProfileActivity"/>
|
android:name=".profile.SingleStatActivity"
|
||||||
|
android:parentActivityName=".profile.ProfileActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".media.manga.mangareader.MangaReaderActivity"
|
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
@@ -149,7 +218,7 @@
|
|||||||
android:name=".media.MediaDetailsActivity"
|
android:name=".media.MediaDetailsActivity"
|
||||||
android:parentActivityName=".MainActivity"
|
android:parentActivityName=".MainActivity"
|
||||||
android:theme="@style/Theme.Dantotsu.NeverCutout"
|
android:theme="@style/Theme.Dantotsu.NeverCutout"
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden"/>
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
<activity android:name=".media.CharacterDetailsActivity" />
|
<activity android:name=".media.CharacterDetailsActivity" />
|
||||||
<activity android:name=".home.NoInternet" />
|
<activity android:name=".home.NoInternet" />
|
||||||
<activity
|
<activity
|
||||||
@@ -231,7 +300,6 @@
|
|||||||
<data android:host="discord.dantotsu.com" />
|
<data android:host="discord.dantotsu.com" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".connections.anilist.UrlMedia"
|
android:name=".connections.anilist.UrlMedia"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
@@ -290,7 +358,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:scheme="content" />
|
<data android:scheme="content" />
|
||||||
<data android:mimeType="*/*" />
|
<data android:mimeType="*/*" />
|
||||||
<data android:pathPattern=".*\\.ani" />
|
<data android:pathPattern=".*\\.ani" />
|
||||||
@@ -299,30 +369,27 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||||
android:exported="false"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
||||||
<activity
|
|
||||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<receiver android:name=".notifications.AlarmPermissionStateReceiver"
|
<receiver
|
||||||
|
android:name=".notifications.AlarmPermissionStateReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
|
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver
|
||||||
<receiver android:name=".notifications.BootCompletedReceiver"
|
android:name=".notifications.BootCompletedReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver"/>
|
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver" />
|
||||||
<receiver android:name=".notifications.comment.CommentNotificationReceiver"/>
|
<receiver android:name=".notifications.comment.CommentNotificationReceiver" />
|
||||||
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver"/>
|
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="preloaded_fonts"
|
android:name="preloaded_fonts"
|
||||||
@@ -340,25 +407,11 @@
|
|||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
<service
|
<service
|
||||||
android:name=".download.video.ExoplayerDownloadService"
|
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="dataSync">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<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:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
<service
|
<service
|
||||||
@@ -381,6 +434,11 @@
|
|||||||
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
<service
|
||||||
|
android:name=".addons.torrent.ServerService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:stopWithTask="true" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.content.Context
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.multidex.MultiDex
|
import androidx.multidex.MultiDex
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
|
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||||
import ani.dantotsu.connections.comments.CommentsAPI
|
import ani.dantotsu.connections.comments.CommentsAPI
|
||||||
@@ -41,6 +43,9 @@ class App : MultiDexApplication() {
|
|||||||
private lateinit var animeExtensionManager: AnimeExtensionManager
|
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||||
private lateinit var mangaExtensionManager: MangaExtensionManager
|
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||||
private lateinit var novelExtensionManager: NovelExtensionManager
|
private lateinit var novelExtensionManager: NovelExtensionManager
|
||||||
|
private lateinit var torrentAddonManager: TorrentAddonManager
|
||||||
|
private lateinit var downloadAddonManager: DownloadAddonManager
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
MultiDex.install(this)
|
MultiDex.install(this)
|
||||||
@@ -86,7 +91,7 @@ class App : MultiDexApplication() {
|
|||||||
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
||||||
Logger.log("App: Logging started")
|
Logger.log("App: Logging started")
|
||||||
|
|
||||||
initializeNetwork(baseContext)
|
initializeNetwork()
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
if (!LogcatLogger.isInstalled) {
|
if (!LogcatLogger.isInstalled) {
|
||||||
@@ -96,6 +101,8 @@ class App : MultiDexApplication() {
|
|||||||
animeExtensionManager = Injekt.get()
|
animeExtensionManager = Injekt.get()
|
||||||
mangaExtensionManager = Injekt.get()
|
mangaExtensionManager = Injekt.get()
|
||||||
novelExtensionManager = Injekt.get()
|
novelExtensionManager = Injekt.get()
|
||||||
|
torrentAddonManager = Injekt.get()
|
||||||
|
downloadAddonManager = Injekt.get()
|
||||||
|
|
||||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||||
animeScope.launch {
|
animeScope.launch {
|
||||||
@@ -115,13 +122,20 @@ class App : MultiDexApplication() {
|
|||||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||||
}
|
}
|
||||||
|
val addonScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
addonScope.launch {
|
||||||
|
torrentAddonManager.init()
|
||||||
|
downloadAddonManager.init()
|
||||||
|
}
|
||||||
val commentsScope = CoroutineScope(Dispatchers.Default)
|
val commentsScope = CoroutineScope(Dispatchers.Default)
|
||||||
commentsScope.launch {
|
commentsScope.launch {
|
||||||
CommentsAPI.fetchAuthToken()
|
CommentsAPI.fetchAuthToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
|
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
|
||||||
TaskScheduler.create(this, useAlarmManager).scheduleAllTasks(this)
|
val scheduler = TaskScheduler.create(this, useAlarmManager)
|
||||||
|
scheduler.scheduleAllTasks(this)
|
||||||
|
scheduler.scheduleSingleWork(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupNotificationChannels() {
|
private fun setupNotificationChannels() {
|
||||||
@@ -152,6 +166,10 @@ class App : MultiDexApplication() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: App? = null
|
private var instance: App? = null
|
||||||
|
|
||||||
|
/** Reference to the application context.
|
||||||
|
*
|
||||||
|
* USE WITH EXTREME CAUTION!**/
|
||||||
var context: Context? = null
|
var context: Context? = null
|
||||||
fun currentContext(): Context? {
|
fun currentContext(): Context? {
|
||||||
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.app.PendingIntent
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -66,12 +67,9 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.math.MathUtils.clamp
|
import androidx.core.math.MathUtils.clamp
|
||||||
@@ -90,6 +88,7 @@ import androidx.viewpager2.widget.ViewPager2
|
|||||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||||
import ani.dantotsu.connections.anilist.Genre
|
import ani.dantotsu.connections.anilist.Genre
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
|
import ani.dantotsu.connections.bakaupdates.MangaUpdates
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
@@ -100,6 +99,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
|||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||||
|
import ani.dantotsu.util.CountUpTimer
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestBuilder
|
import com.bumptech.glide.RequestBuilder
|
||||||
@@ -132,9 +132,12 @@ import io.noties.markwon.html.TagHandlerNoOp
|
|||||||
import io.noties.markwon.image.AsyncDrawable
|
import io.noties.markwon.image.AsyncDrawable
|
||||||
import io.noties.markwon.image.glide.GlideImagesPlugin
|
import io.noties.markwon.image.glide.GlideImagesPlugin
|
||||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -182,6 +185,11 @@ fun currActivity(): Activity? {
|
|||||||
var loadMedia: Int? = null
|
var loadMedia: Int? = null
|
||||||
var loadIsMAL = false
|
var loadIsMAL = false
|
||||||
|
|
||||||
|
val Int.toPx
|
||||||
|
get() = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
|
||||||
|
).toInt()
|
||||||
|
|
||||||
fun initActivity(a: Activity) {
|
fun initActivity(a: Activity) {
|
||||||
val window = a.window
|
val window = a.window
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
@@ -201,13 +209,16 @@ fun initActivity(a: Activity) {
|
|||||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||||
?.apply {
|
?.apply {
|
||||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowInsetsControllerCompat(
|
WindowInsetsControllerCompat(
|
||||||
window,
|
window,
|
||||||
window.decorView
|
window.decorView
|
||||||
).hide(WindowInsetsCompat.Type.statusBars())
|
).hide(WindowInsetsCompat.Type.statusBars())
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
|
||||||
|
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||||
|
) {
|
||||||
window.decorView.rootWindowInsets?.displayCutout?.apply {
|
window.decorView.rootWindowInsets?.displayCutout?.apply {
|
||||||
if (boundingRects.size > 0) {
|
if (boundingRects.size > 0) {
|
||||||
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
|
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
|
||||||
@@ -222,6 +233,7 @@ fun initActivity(a: Activity) {
|
|||||||
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||||
navBarHeight =
|
navBarHeight =
|
||||||
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (a !is MainActivity) a.setNavigationTheme()
|
if (a !is MainActivity) a.setNavigationTheme()
|
||||||
@@ -262,6 +274,56 @@ fun Activity.setNavigationTheme() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
|
||||||
|
*
|
||||||
|
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
|
||||||
|
*/
|
||||||
|
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar) {
|
||||||
|
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
clipToPadding = false
|
||||||
|
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
|
||||||
|
*
|
||||||
|
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
|
||||||
|
*/
|
||||||
|
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar, overlayView: View) {
|
||||||
|
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
overlayView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
clipToPadding = false
|
||||||
|
setPadding(
|
||||||
|
paddingLeft,
|
||||||
|
paddingTop,
|
||||||
|
paddingRight,
|
||||||
|
navBarHeight + navBar.measuredHeight + overlayView.measuredHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.reloadActivity() {
|
||||||
|
Refresh.all()
|
||||||
|
finish()
|
||||||
|
startActivity(Intent(this, this::class.java))
|
||||||
|
initActivity(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.restartApp() {
|
||||||
|
val mainIntent = Intent.makeRestartActivityTask(
|
||||||
|
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
|
||||||
|
)
|
||||||
|
val component =
|
||||||
|
ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
|
||||||
|
try {
|
||||||
|
startActivity(Intent().setComponent(component))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
startActivity(mainIntent)
|
||||||
|
}
|
||||||
|
finishAndRemoveTask()
|
||||||
|
PrefManager.setCustomVal("reload", true)
|
||||||
|
}
|
||||||
|
|
||||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
@@ -359,7 +421,7 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
|
|||||||
dialog.setButton(
|
dialog.setButton(
|
||||||
DialogInterface.BUTTON_NEUTRAL,
|
DialogInterface.BUTTON_NEUTRAL,
|
||||||
activity.getString(R.string.remove)
|
activity.getString(R.string.remove)
|
||||||
) { dialog, which ->
|
) { _, which ->
|
||||||
if (which == DialogInterface.BUTTON_NEUTRAL) {
|
if (which == DialogInterface.BUTTON_NEUTRAL) {
|
||||||
date = FuzzyDate()
|
date = FuzzyDate()
|
||||||
}
|
}
|
||||||
@@ -394,7 +456,6 @@ class InputFilterMinMax(
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
private fun isInRange(a: Double, b: Double, c: Double): Boolean {
|
private fun isInRange(a: Double, b: Double, c: Double): Boolean {
|
||||||
val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2]
|
val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2]
|
||||||
|
|
||||||
@@ -407,7 +468,7 @@ class InputFilterMinMax(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ZoomOutPageTransformer() :
|
class ZoomOutPageTransformer :
|
||||||
ViewPager2.PageTransformer {
|
ViewPager2.PageTransformer {
|
||||||
override fun transformPage(view: View, position: Float) {
|
override fun transformPage(view: View, position: Float) {
|
||||||
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
|
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||||
@@ -563,13 +624,35 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
|||||||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||||
if (file?.url?.isNotEmpty() == true) {
|
if (file?.url?.isNotEmpty() == true) {
|
||||||
tryWith {
|
tryWith {
|
||||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
if (file.url.startsWith("content://")) {
|
||||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
|
||||||
.into(this)
|
.override(size).into(this)
|
||||||
|
} else {
|
||||||
|
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||||
|
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||||
|
.into(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ImageView.loadImage(file: FileUrl?, width: Int = 0, height: Int = 0) {
|
||||||
|
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||||
|
if (file?.url?.isNotEmpty() == true) {
|
||||||
|
tryWith {
|
||||||
|
if (file.url.startsWith("content://")) {
|
||||||
|
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
|
||||||
|
.override(width, height).into(this)
|
||||||
|
} else {
|
||||||
|
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||||
|
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(width, height)
|
||||||
|
.into(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
||||||
if (file?.exists() == true) {
|
if (file?.exists() == true) {
|
||||||
tryWith {
|
tryWith {
|
||||||
@@ -712,6 +795,23 @@ fun openLinkInBrowser(link: String?) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openLinkInYouTube(link: String?) {
|
||||||
|
link?.let {
|
||||||
|
try {
|
||||||
|
val videoIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||||
|
data = Uri.parse(link)
|
||||||
|
setPackage("com.google.android.youtube")
|
||||||
|
}
|
||||||
|
currContext()!!.startActivity(videoIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
openLinkInBrowser(link)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) {
|
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) {
|
||||||
FileProvider.getUriForFile(
|
FileProvider.getUriForFile(
|
||||||
context,
|
context,
|
||||||
@@ -803,31 +903,6 @@ fun savePrefs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
|
|
||||||
val permissions = arrayOf(
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
)
|
|
||||||
|
|
||||||
val requiredPermissions = permissions.filter {
|
|
||||||
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
return if (requiredPermissions.isNotEmpty()) {
|
|
||||||
ActivityCompat.requestPermissions(
|
|
||||||
activity,
|
|
||||||
requiredPermissions,
|
|
||||||
DOWNLOADS_PERMISSION_REQUEST_CODE
|
|
||||||
)
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
|
|
||||||
|
|
||||||
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||||
|
|
||||||
val contentUri = FileProvider.getUriForFile(
|
val contentUri = FileProvider.getUriForFile(
|
||||||
@@ -897,9 +972,10 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
fun countDown(media: Media, view: ViewGroup) {
|
fun countDown(media: Media, view: ViewGroup) {
|
||||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
|
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
|
||||||
|
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()
|
||||||
|
) {
|
||||||
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||||
view.addView(v.root, 0)
|
view.addView(v.root, 0)
|
||||||
v.mediaCountdownText.text =
|
v.mediaCountdownText.text =
|
||||||
@@ -931,6 +1007,50 @@ fun countDown(media: Media, view: ViewGroup) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sinceWhen(media: Media, view: ViewGroup) {
|
||||||
|
if (media.status != "RELEASING" && media.status != "HIATUS") return
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
|
||||||
|
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
|
||||||
|
val timeSince = (System.currentTimeMillis() -
|
||||||
|
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val v =
|
||||||
|
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||||
|
view.addView(v.root, 0)
|
||||||
|
v.mediaCountdownText.text =
|
||||||
|
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
|
||||||
|
|
||||||
|
object : CountUpTimer(86400000) {
|
||||||
|
override fun onTick(second: Int) {
|
||||||
|
val a = second + timeSince
|
||||||
|
v.mediaCountdown.text = currActivity()?.getString(
|
||||||
|
R.string.time_format,
|
||||||
|
a / 86400,
|
||||||
|
a % 86400 / 3600,
|
||||||
|
a % 86400 % 3600 / 60,
|
||||||
|
a % 86400 % 3600 % 60
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
// The legend will never die.
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayTimer(media: Media, view: ViewGroup) {
|
||||||
|
when {
|
||||||
|
media.anime != null -> countDown(media, view)
|
||||||
|
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
|
||||||
|
else -> {} // No timer yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
|
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
|
||||||
this.forEach {
|
this.forEach {
|
||||||
if (it.value.id == id) {
|
if (it.value.id == id) {
|
||||||
@@ -1000,6 +1120,10 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
|
|||||||
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
|
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAppString(res: Int): String {
|
||||||
|
return currContext()?.getString(res) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
fun toast(string: String?) {
|
fun toast(string: String?) {
|
||||||
if (string != null) {
|
if (string != null) {
|
||||||
Logger.log(string)
|
Logger.log(string)
|
||||||
@@ -1010,6 +1134,10 @@ fun toast(string: String?) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toast(res: Int) {
|
||||||
|
toast(getAppString(res))
|
||||||
|
}
|
||||||
|
|
||||||
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
|
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
|
||||||
try { //I have no idea why this sometimes crashes for some people...
|
try { //I have no idea why this sometimes crashes for some people...
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
@@ -1050,6 +1178,10 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun snackString(r: Int, activity: Activity? = null, clipboard: String? = null): Snackbar? {
|
||||||
|
return snackString(getAppString(r), activity, clipboard)
|
||||||
|
}
|
||||||
|
|
||||||
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
|
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
|
||||||
ArrayAdapter<T>(context, layoutId, items) {
|
ArrayAdapter<T>(context, layoutId, items) {
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
@@ -1235,8 +1367,12 @@ fun blurImage(imageView: ImageView, banner: String?) {
|
|||||||
if (!(context as Activity).isDestroyed) {
|
if (!(context as Activity).isDestroyed) {
|
||||||
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
|
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
|
||||||
Glide.with(context as Context)
|
Glide.with(context as Context)
|
||||||
.load(GlideUrl(url))
|
.load(
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
|
||||||
|
url
|
||||||
|
) else File(url)
|
||||||
|
)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
|
||||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
}
|
}
|
||||||
@@ -1321,4 +1457,4 @@ fun buildMarkwon(
|
|||||||
}))
|
}))
|
||||||
.build()
|
.build()
|
||||||
return markwon
|
return markwon
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -14,13 +13,11 @@ import android.os.Bundle
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnticipateInterpolator
|
import android.view.animation.AnticipateInterpolator
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
@@ -36,14 +33,15 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.offline.Download
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import ani.dantotsu.addons.torrent.ServerService
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||||
import ani.dantotsu.databinding.ActivityMainBinding
|
import ani.dantotsu.databinding.ActivityMainBinding
|
||||||
|
import ani.dantotsu.databinding.DialogUserAgentBinding
|
||||||
import ani.dantotsu.databinding.SplashScreenBinding
|
import ani.dantotsu.databinding.SplashScreenBinding
|
||||||
import ani.dantotsu.download.video.Helper
|
|
||||||
import ani.dantotsu.home.AnimeFragment
|
import ani.dantotsu.home.AnimeFragment
|
||||||
import ani.dantotsu.home.HomeFragment
|
import ani.dantotsu.home.HomeFragment
|
||||||
import ani.dantotsu.home.LoginFragment
|
import ani.dantotsu.home.LoginFragment
|
||||||
@@ -56,6 +54,7 @@ import ani.dantotsu.others.CustomBottomDialog
|
|||||||
import ani.dantotsu.profile.ProfileActivity
|
import ani.dantotsu.profile.ProfileActivity
|
||||||
import ani.dantotsu.profile.activity.FeedActivity
|
import ani.dantotsu.profile.activity.FeedActivity
|
||||||
import ani.dantotsu.profile.activity.NotificationActivity
|
import ani.dantotsu.profile.activity.NotificationActivity
|
||||||
|
import ani.dantotsu.settings.ExtensionsActivity
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
@@ -70,11 +69,13 @@ import com.google.android.material.textfield.TextInputEditText
|
|||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
@@ -87,6 +88,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private var load = false
|
private var load = false
|
||||||
|
|
||||||
|
|
||||||
|
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
||||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -161,16 +163,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
|
||||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
val backgroundDrawable = bottomNavBar.background as GradientDrawable
|
||||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||||
backgroundDrawable.setColor(semiTransparentColor)
|
backgroundDrawable.setColor(semiTransparentColor)
|
||||||
_bottomBar.background = backgroundDrawable
|
bottomNavBar.background = backgroundDrawable
|
||||||
}
|
}
|
||||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||||
|
|
||||||
val offset = try {
|
val offset = try {
|
||||||
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||||
@@ -230,17 +232,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val preferences: SourcePreferences = Injekt.get()
|
|
||||||
if (preferences.animeExtensionUpdatesCount()
|
|
||||||
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
|
||||||
) {
|
|
||||||
Toast.makeText(
|
|
||||||
this,
|
|
||||||
"You have extension updates available!",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.isMotionEventSplittingEnabled = false
|
binding.root.isMotionEventSplittingEnabled = false
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -284,6 +275,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
binding.root.doOnAttach {
|
binding.root.doOnAttach {
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
|
val preferences: SourcePreferences = Injekt.get()
|
||||||
|
if (preferences.animeExtensionUpdatesCount()
|
||||||
|
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
||||||
|
) {
|
||||||
|
snackString(R.string.extension_updates_available)
|
||||||
|
?.setDuration(Snackbar.LENGTH_LONG)
|
||||||
|
?.setAction(R.string.review) {
|
||||||
|
startActivity(Intent(this, ExtensionsActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
||||||
selectedOption = if (fragment != null) {
|
selectedOption = if (fragment != null) {
|
||||||
when (fragment) {
|
when (fragment) {
|
||||||
@@ -300,6 +301,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var launched = false
|
||||||
intent.extras?.let { extras ->
|
intent.extras?.let { extras ->
|
||||||
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
|
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
|
||||||
val mediaId = extras.getInt("mediaId", -1)
|
val mediaId = extras.getInt("mediaId", -1)
|
||||||
@@ -312,6 +314,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
putExtra("mediaId", mediaId)
|
putExtra("mediaId", mediaId)
|
||||||
putExtra("commentId", commentId)
|
putExtra("commentId", commentId)
|
||||||
}
|
}
|
||||||
|
launched = true
|
||||||
startActivity(detailIntent)
|
startActivity(detailIntent)
|
||||||
} else if (fragmentToLoad == "FEED" && activityId != -1) {
|
} else if (fragmentToLoad == "FEED" && activityId != -1) {
|
||||||
val feedIntent = Intent(this, FeedActivity::class.java).apply {
|
val feedIntent = Intent(this, FeedActivity::class.java).apply {
|
||||||
@@ -319,6 +322,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
putExtra("activityId", activityId)
|
putExtra("activityId", activityId)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
launched = true
|
||||||
startActivity(feedIntent)
|
startActivity(feedIntent)
|
||||||
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
||||||
Logger.log("MainActivity, onCreate: $activityId")
|
Logger.log("MainActivity, onCreate: $activityId")
|
||||||
@@ -326,6 +330,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
||||||
putExtra("activityId", activityId)
|
putExtra("activityId", activityId)
|
||||||
}
|
}
|
||||||
|
launched = true
|
||||||
startActivity(notificationIntent)
|
startActivity(notificationIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +344,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(this, NoInternet::class.java))
|
startActivity(Intent(this, NoInternet::class.java))
|
||||||
} else {
|
} else {
|
||||||
val model: AnilistHomeViewModel by viewModels()
|
val model: AnilistHomeViewModel by viewModels()
|
||||||
model.genres.observe(this) { it ->
|
model.genres.observe(this) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
if (it) {
|
if (it) {
|
||||||
val navbar = binding.includedNavbar.navbar
|
val navbar = binding.includedNavbar.navbar
|
||||||
@@ -364,7 +369,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
mainViewPager.setCurrentItem(newIndex, false)
|
mainViewPager.setCurrentItem(newIndex, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (mainViewPager.getCurrentItem() != selectedOption) {
|
if (mainViewPager.currentItem != selectedOption) {
|
||||||
navbar.selectTabAt(selectedOption)
|
navbar.selectTabAt(selectedOption)
|
||||||
mainViewPager.post {
|
mainViewPager.post {
|
||||||
mainViewPager.setCurrentItem(
|
mainViewPager.setCurrentItem(
|
||||||
@@ -379,7 +384,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Load Data
|
//Load Data
|
||||||
if (!load) {
|
if (!load && !launched) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
model.loadMain(this@MainActivity)
|
model.loadMain(this@MainActivity)
|
||||||
val id = intent.extras?.getInt("mediaId", 0)
|
val id = intent.extras?.getInt("mediaId", 0)
|
||||||
@@ -450,16 +455,26 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
|
|
||||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
val torrentManager = Injekt.get<TorrentAddonManager>()
|
||||||
val downloadCursor = index.getDownloads()
|
fun startTorrent() {
|
||||||
while (downloadCursor.moveToNext()) {
|
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
|
||||||
val download = downloadCursor.download
|
launchIO {
|
||||||
if (download.state == Download.STATE_FAILED) {
|
if (!ServerService.isRunning()) {
|
||||||
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
ServerService.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (torrentManager.isInitialized.value == false) {
|
||||||
|
torrentManager.isInitialized.observe(this) {
|
||||||
|
if (it) {
|
||||||
|
startTorrent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startTorrent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestart() {
|
override fun onRestart() {
|
||||||
@@ -467,34 +482,26 @@ class MainActivity : AppCompatActivity() {
|
|||||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Int.toPx get() = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
|
|
||||||
).toInt()
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
val params : ViewGroup.MarginLayoutParams =
|
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
|
||||||
|
val params: ViewGroup.MarginLayoutParams =
|
||||||
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
|
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
params.updateMargins(bottom = margin.toPx)
|
||||||
params.updateMargins(bottom = 8.toPx)
|
|
||||||
else
|
|
||||||
params.updateMargins(bottom = 32.toPx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
||||||
val password = CharArray(16).apply { fill('0') }
|
val password = CharArray(16).apply { fill('0') }
|
||||||
|
|
||||||
// Inflate the dialog layout
|
// Inflate the dialog layout
|
||||||
val dialogView =
|
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||||
LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null)
|
dialogView.userAgentTextBox.hint = "Password"
|
||||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
dialogView.subtitle.visibility = View.VISIBLE
|
||||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
|
||||||
subtitleTextView?.visibility = View.VISIBLE
|
|
||||||
subtitleTextView?.text = "Enter your password to decrypt the file"
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||||
.setTitle("Enter Password")
|
.setTitle("Enter Password")
|
||||||
.setView(dialogView)
|
.setView(dialogView.root)
|
||||||
.setPositiveButton("OK", null)
|
.setPositiveButton("OK", null)
|
||||||
.setNegativeButton("Cancel") { dialog, _ ->
|
.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
password.fill('0')
|
password.fill('0')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu
|
package ani.dantotsu
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import ani.dantotsu.others.webview.CloudFlare
|
import ani.dantotsu.others.webview.CloudFlare
|
||||||
@@ -10,6 +9,7 @@ import com.lagradost.nicehttp.Requests
|
|||||||
import com.lagradost.nicehttp.ResponseParser
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
import com.lagradost.nicehttp.addGenericDns
|
import com.lagradost.nicehttp.addGenericDns
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -35,13 +35,13 @@ lateinit var defaultHeaders: Map<String, String>
|
|||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var okHttpClient: OkHttpClient
|
||||||
lateinit var client: Requests
|
lateinit var client: Requests
|
||||||
|
|
||||||
fun initializeNetwork(context: Context) {
|
fun initializeNetwork() {
|
||||||
|
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
|
||||||
defaultHeaders = mapOf(
|
defaultHeaders = mapOf(
|
||||||
"User-Agent" to
|
"User-Agent" to
|
||||||
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
defaultUserAgentProvider()
|
||||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package ani.dantotsu.addons
|
||||||
|
|
||||||
|
abstract class Addon {
|
||||||
|
abstract val name: String
|
||||||
|
abstract val pkgName: String
|
||||||
|
abstract val versionName: String
|
||||||
|
abstract val versionCode: Long
|
||||||
|
|
||||||
|
abstract class Installed(
|
||||||
|
override val name: String,
|
||||||
|
override val pkgName: String,
|
||||||
|
override val versionName: String,
|
||||||
|
override val versionCode: Long,
|
||||||
|
) : Addon()
|
||||||
|
}
|
||||||
128
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
128
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package ani.dantotsu.addons
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import ani.dantotsu.Mapper
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.logError
|
||||||
|
import ani.dantotsu.openLinkInBrowser
|
||||||
|
import ani.dantotsu.others.AppUpdater
|
||||||
|
import ani.dantotsu.settings.InstallerSteps
|
||||||
|
import ani.dantotsu.toast
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
|
||||||
|
class AddonDownloader {
|
||||||
|
companion object {
|
||||||
|
private suspend fun check(repo: String): Pair<String, String> {
|
||||||
|
return try {
|
||||||
|
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||||
|
.parsed<JsonArray>().map {
|
||||||
|
Mapper.json.decodeFromJsonElement<AppUpdater.GithubResponse>(it)
|
||||||
|
}
|
||||||
|
val r = res.maxByOrNull {
|
||||||
|
it.timeStamp()
|
||||||
|
} ?: throw Exception("No Pre Release Found")
|
||||||
|
val v = r.tagName.substringAfter("v", "")
|
||||||
|
val md = r.body ?: ""
|
||||||
|
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||||
|
|
||||||
|
Logger.log("Git Version : $version")
|
||||||
|
Pair(md, version)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.log("Error checking for update")
|
||||||
|
Logger.log(e)
|
||||||
|
Pair("", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun hasUpdate(repo: String, currentVersion: String): Boolean {
|
||||||
|
val (_, version) = check(repo)
|
||||||
|
return compareVersion(version, currentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(
|
||||||
|
activity: Activity,
|
||||||
|
manager: AddonManager<*>,
|
||||||
|
repo: String,
|
||||||
|
currentVersion: String
|
||||||
|
) {
|
||||||
|
val (_, version) = check(repo)
|
||||||
|
if (!compareVersion(version, currentVersion)) {
|
||||||
|
toast(activity.getString(R.string.no_update_found))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MainScope().launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val apks =
|
||||||
|
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||||
|
.parsed<AppUpdater.GithubResponse>().assets?.filter {
|
||||||
|
it.browserDownloadURL.endsWith(
|
||||||
|
".apk"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val apkToDownload =
|
||||||
|
apks?.find { it.browserDownloadURL.contains(getCurrentABI()) }
|
||||||
|
?: apks?.find { it.browserDownloadURL.contains("universal") }
|
||||||
|
?: apks?.first()
|
||||||
|
apkToDownload?.browserDownloadURL.apply {
|
||||||
|
if (this != null) {
|
||||||
|
val notificationManager =
|
||||||
|
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val installerSteps = InstallerSteps(notificationManager, activity)
|
||||||
|
manager.install(this)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||||
|
{ error -> installerSteps.onError(error) {} },
|
||||||
|
{ installerSteps.onComplete {} }
|
||||||
|
)
|
||||||
|
} else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ABI that the app is most likely running on.
|
||||||
|
* @return The primary ABI for the device.
|
||||||
|
*/
|
||||||
|
private fun getCurrentABI(): String {
|
||||||
|
return if (Build.SUPPORTED_ABIS.isNotEmpty()) {
|
||||||
|
Build.SUPPORTED_ABIS[0]
|
||||||
|
} else "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareVersion(newVersion: String, oldVersion: String): Boolean {
|
||||||
|
fun toDouble(list: List<String>): Double {
|
||||||
|
return try {
|
||||||
|
list.mapIndexed { i: Int, s: String ->
|
||||||
|
when (i) {
|
||||||
|
0 -> s.toDouble() * 100
|
||||||
|
1 -> s.toDouble() * 10
|
||||||
|
2 -> s.toDouble()
|
||||||
|
else -> s.toDoubleOrNull() ?: 0.0
|
||||||
|
}
|
||||||
|
}.sum()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val new = toDouble(newVersion.split("."))
|
||||||
|
val curr = toDouble(oldVersion.split("."))
|
||||||
|
return new > curr
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
131
app/src/main/java/ani/dantotsu/addons/AddonInstallReceiver.kt
Normal file
131
app/src/main/java/ani/dantotsu/addons/AddonInstallReceiver.kt
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package ani.dantotsu.addons
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||||
|
import ani.dantotsu.media.AddonType
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.filter
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.getPackageNameFromIntent
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import tachiyomi.core.util.lang.launchNow
|
||||||
|
|
||||||
|
internal class AddonInstallReceiver : BroadcastReceiver() {
|
||||||
|
private var listener: AddonListener? = null
|
||||||
|
private var type: AddonType? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers this broadcast receiver
|
||||||
|
*/
|
||||||
|
fun register(context: Context) {
|
||||||
|
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setListener(listener: AddonListener, type: AddonType): AddonInstallReceiver {
|
||||||
|
this.listener = listener
|
||||||
|
this.type = type
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when one of the events of the [filter] is received. When the package is an extension,
|
||||||
|
* it's loaded in background and it notifies the [listener] when finished.
|
||||||
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
if (intent == null) return
|
||||||
|
|
||||||
|
when (intent.action) {
|
||||||
|
Intent.ACTION_PACKAGE_ADDED -> {
|
||||||
|
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||||
|
launchNow {
|
||||||
|
when (type) {
|
||||||
|
AddonType.DOWNLOAD -> {
|
||||||
|
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||||
|
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||||
|
listener?.onAddonInstalled(
|
||||||
|
AddonLoader.loadFromPkgName(
|
||||||
|
context,
|
||||||
|
packageName,
|
||||||
|
AddonType.DOWNLOAD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonType.TORRENT -> {
|
||||||
|
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||||
|
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||||
|
listener?.onAddonInstalled(
|
||||||
|
AddonLoader.loadFromPkgName(
|
||||||
|
context,
|
||||||
|
packageName,
|
||||||
|
AddonType.TORRENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||||
|
launchNow {
|
||||||
|
when (type) {
|
||||||
|
AddonType.DOWNLOAD -> {
|
||||||
|
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||||
|
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||||
|
listener?.onAddonUpdated(
|
||||||
|
AddonLoader.loadFromPkgName(
|
||||||
|
context,
|
||||||
|
packageName,
|
||||||
|
AddonType.DOWNLOAD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonType.TORRENT -> {
|
||||||
|
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||||
|
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||||
|
listener?.onAddonUpdated(
|
||||||
|
AddonLoader.loadFromPkgName(
|
||||||
|
context,
|
||||||
|
packageName,
|
||||||
|
AddonType.TORRENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||||
|
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||||
|
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||||
|
when (type) {
|
||||||
|
AddonType.DOWNLOAD -> {
|
||||||
|
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return
|
||||||
|
listener?.onAddonUninstalled(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonType.TORRENT -> {
|
||||||
|
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return
|
||||||
|
listener?.onAddonUninstalled(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package ani.dantotsu.addons
|
||||||
|
|
||||||
|
interface AddonListener {
|
||||||
|
fun onAddonInstalled(result: LoadResult?)
|
||||||
|
fun onAddonUpdated(result: LoadResult?)
|
||||||
|
fun onAddonUninstalled(pkgName: String)
|
||||||
|
|
||||||
|
enum class ListenerAction {
|
||||||
|
INSTALL, UPDATE, UNINSTALL
|
||||||
|
}
|
||||||
|
}
|
||||||
143
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
143
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package ani.dantotsu.addons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
|
import ani.dantotsu.addons.download.DownloadAddon
|
||||||
|
import ani.dantotsu.addons.download.DownloadAddonApi
|
||||||
|
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||||
|
import ani.dantotsu.addons.download.DownloadLoadResult
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentAddon
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentAddonApi
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentLoadResult
|
||||||
|
import ani.dantotsu.media.AddonType
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
|
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||||
|
|
||||||
|
class AddonLoader {
|
||||||
|
companion object {
|
||||||
|
fun loadExtension(
|
||||||
|
context: Context,
|
||||||
|
packageName: String,
|
||||||
|
className: String,
|
||||||
|
type: AddonType
|
||||||
|
): LoadResult? {
|
||||||
|
val pkgManager = context.packageManager
|
||||||
|
|
||||||
|
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(ExtensionLoader.PACKAGE_FLAGS.toLong()))
|
||||||
|
} else {
|
||||||
|
pkgManager.getInstalledPackages(ExtensionLoader.PACKAGE_FLAGS)
|
||||||
|
}
|
||||||
|
|
||||||
|
val extPkgs = installedPkgs.filter {
|
||||||
|
isPackageAnExtension(
|
||||||
|
packageName,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extPkgs.isEmpty()) return null
|
||||||
|
if (extPkgs.size > 1) throw IllegalStateException("Multiple extensions with the same package name found")
|
||||||
|
|
||||||
|
val pkgName = extPkgs.first().packageName
|
||||||
|
val pkgInfo = extPkgs.first()
|
||||||
|
|
||||||
|
val appInfo = try {
|
||||||
|
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||||
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
|
// Unlikely, but the package may have been uninstalled at this point
|
||||||
|
Logger.log(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
val extName =
|
||||||
|
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Dantotsu: ")
|
||||||
|
val versionName = pkgInfo.versionName
|
||||||
|
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||||
|
|
||||||
|
if (versionName.isNullOrEmpty()) {
|
||||||
|
Logger.log("Missing versionName for extension $extName")
|
||||||
|
throw IllegalStateException("Missing versionName for extension $extName")
|
||||||
|
}
|
||||||
|
val classLoader =
|
||||||
|
PathClassLoader(appInfo.sourceDir, appInfo.nativeLibraryDir, context.classLoader)
|
||||||
|
val loadedClass = try {
|
||||||
|
Class.forName(className, false, classLoader)
|
||||||
|
} catch (e: ClassNotFoundException) {
|
||||||
|
Logger.log("Extension load error: $extName ($className)")
|
||||||
|
Logger.log(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
Logger.log("Extension load error: $extName ($className)")
|
||||||
|
Logger.log(e)
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.log("Extension load error: $extName ($className)")
|
||||||
|
Logger.log(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||||
|
|
||||||
|
return when (type) {
|
||||||
|
AddonType.TORRENT -> {
|
||||||
|
val extension = instance as? TorrentAddonApi
|
||||||
|
?: throw IllegalStateException("Extension is not a TorrentAddonApi")
|
||||||
|
TorrentLoadResult.Success(
|
||||||
|
TorrentAddon.Installed(
|
||||||
|
name = extName,
|
||||||
|
pkgName = pkgName,
|
||||||
|
versionName = versionName,
|
||||||
|
versionCode = versionCode,
|
||||||
|
extension = extension,
|
||||||
|
icon = context.getApplicationIcon(pkgName),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonType.DOWNLOAD -> {
|
||||||
|
val extension = instance as? DownloadAddonApi
|
||||||
|
?: throw IllegalStateException("Extension is not a DownloadAddonApi")
|
||||||
|
DownloadLoadResult.Success(
|
||||||
|
DownloadAddon.Installed(
|
||||||
|
name = extName,
|
||||||
|
pkgName = pkgName,
|
||||||
|
versionName = versionName,
|
||||||
|
versionCode = versionCode,
|
||||||
|
extension = extension,
|
||||||
|
icon = context.getApplicationIcon(pkgName),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
|
||||||
|
return when (type) {
|
||||||
|
AddonType.TORRENT -> loadExtension(
|
||||||
|
context,
|
||||||
|
packageName,
|
||||||
|
TorrentAddonManager.TORRENT_CLASS,
|
||||||
|
type
|
||||||
|
)
|
||||||
|
|
||||||
|
AddonType.DOWNLOAD -> loadExtension(
|
||||||
|
context,
|
||||||
|
packageName,
|
||||||
|
DownloadAddonManager.DOWNLOAD_CLASS,
|
||||||
|
type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
|
||||||
|
return pkgInfo.packageName.equals(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
46
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package ani.dantotsu.addons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import ani.dantotsu.media.AddonType
|
||||||
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
abstract class AddonManager<T : Addon.Installed>(
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
abstract var extension: T?
|
||||||
|
abstract var name: String
|
||||||
|
abstract var type: AddonType
|
||||||
|
protected val installer by lazy { ExtensionInstaller(context) }
|
||||||
|
var hasUpdate: Boolean = false
|
||||||
|
protected set
|
||||||
|
|
||||||
|
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
|
||||||
|
|
||||||
|
abstract suspend fun init()
|
||||||
|
abstract fun isAvailable(): Boolean
|
||||||
|
abstract fun getVersion(): String?
|
||||||
|
abstract fun getPackageName(): String?
|
||||||
|
abstract fun hadError(context: Context): String?
|
||||||
|
abstract fun updateInstallStep(id: Long, step: InstallStep)
|
||||||
|
abstract fun setInstalling(id: Long)
|
||||||
|
|
||||||
|
fun uninstall() {
|
||||||
|
getPackageName()?.let {
|
||||||
|
installer.uninstallApk(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addListenerAction(action: (AddonListener.ListenerAction) -> Unit) {
|
||||||
|
onListenerAction = action
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListenerAction() {
|
||||||
|
onListenerAction = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun install(url: String): Observable<InstallStep> {
|
||||||
|
return installer.downloadAndInstall(url, getPackageName() ?: "", name, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package ani.dantotsu.addons
|
||||||
|
|
||||||
|
abstract class LoadResult {
|
||||||
|
|
||||||
|
abstract class Success : LoadResult()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ani.dantotsu.addons.download
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import ani.dantotsu.addons.Addon
|
||||||
|
|
||||||
|
sealed class DownloadAddon : Addon() {
|
||||||
|
|
||||||
|
data class Installed(
|
||||||
|
override val name: String,
|
||||||
|
override val pkgName: String,
|
||||||
|
override val versionName: String,
|
||||||
|
override val versionCode: Long,
|
||||||
|
val extension: DownloadAddonApi,
|
||||||
|
val icon: Drawable?,
|
||||||
|
val hasUpdate: Boolean = false,
|
||||||
|
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ani.dantotsu.addons.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
interface DownloadAddonApi {
|
||||||
|
|
||||||
|
fun cancelDownload(sessionId: Long)
|
||||||
|
|
||||||
|
fun setDownloadPath(context: Context, uri: Uri): String
|
||||||
|
|
||||||
|
suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit)
|
||||||
|
|
||||||
|
suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long
|
||||||
|
|
||||||
|
fun getState(sessionId: Long): String
|
||||||
|
|
||||||
|
fun getStackTrace(sessionId: Long): String?
|
||||||
|
|
||||||
|
fun hadError(sessionId: Long): Boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package ani.dantotsu.addons.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.addons.AddonDownloader
|
||||||
|
import ani.dantotsu.addons.AddonInstallReceiver
|
||||||
|
import ani.dantotsu.addons.AddonListener
|
||||||
|
import ani.dantotsu.addons.AddonLoader
|
||||||
|
import ani.dantotsu.addons.AddonManager
|
||||||
|
import ani.dantotsu.addons.LoadResult
|
||||||
|
import ani.dantotsu.media.AddonType
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class DownloadAddonManager(
|
||||||
|
private val context: Context
|
||||||
|
) : AddonManager<DownloadAddon.Installed>(context) {
|
||||||
|
|
||||||
|
override var extension: DownloadAddon.Installed? = null
|
||||||
|
override var name: String = "Download Addon"
|
||||||
|
override var type = AddonType.DOWNLOAD
|
||||||
|
|
||||||
|
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||||
|
|
||||||
|
private var error: String? = null
|
||||||
|
|
||||||
|
override suspend fun init() {
|
||||||
|
extension = null
|
||||||
|
error = null
|
||||||
|
hasUpdate = false
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_isInitialized.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonInstallReceiver()
|
||||||
|
.setListener(InstallationListener(), type)
|
||||||
|
.register(context)
|
||||||
|
try {
|
||||||
|
val result = AddonLoader.loadExtension(
|
||||||
|
context,
|
||||||
|
DOWNLOAD_PACKAGE,
|
||||||
|
DOWNLOAD_CLASS,
|
||||||
|
AddonType.DOWNLOAD
|
||||||
|
) as? DownloadLoadResult
|
||||||
|
result?.let {
|
||||||
|
if (it is DownloadLoadResult.Success) {
|
||||||
|
extension = it.extension
|
||||||
|
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_isInitialized.value = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.log("Error initializing Download extension")
|
||||||
|
Logger.log(e)
|
||||||
|
error = e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return extension?.extension != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVersion(): String? {
|
||||||
|
return extension?.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPackageName(): String? {
|
||||||
|
return extension?.pkgName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hadError(context: Context): String? {
|
||||||
|
return if (isInitialized.value == true) {
|
||||||
|
if (error != null) {
|
||||||
|
error
|
||||||
|
} else if (extension != null) {
|
||||||
|
context.getString(R.string.loaded_successfully)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class InstallationListener : AddonListener {
|
||||||
|
override fun onAddonInstalled(result: LoadResult?) {
|
||||||
|
if (result is DownloadLoadResult.Success) {
|
||||||
|
extension = result.extension
|
||||||
|
hasUpdate = false
|
||||||
|
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddonUpdated(result: LoadResult?) {
|
||||||
|
if (result is DownloadLoadResult.Success) {
|
||||||
|
extension = result.extension
|
||||||
|
hasUpdate = false
|
||||||
|
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddonUninstalled(pkgName: String) {
|
||||||
|
if (extension?.pkgName == pkgName) {
|
||||||
|
extension = null
|
||||||
|
hasUpdate = false
|
||||||
|
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||||
|
installer.updateInstallStep(id, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setInstalling(id: Long) {
|
||||||
|
installer.updateInstallStep(id, InstallStep.Installing)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val DOWNLOAD_PACKAGE = "dantotsu.downloadAddon"
|
||||||
|
const val DOWNLOAD_CLASS = "ani.dantotsu.downloadAddon.DownloadAddon"
|
||||||
|
const val REPO = "rebelonion/Dantotsu-Download-Addon"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ani.dantotsu.addons.download
|
||||||
|
|
||||||
|
import ani.dantotsu.addons.LoadResult
|
||||||
|
|
||||||
|
open class DownloadLoadResult : LoadResult() {
|
||||||
|
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ani.dantotsu.addons.torrent
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import ani.dantotsu.addons.Addon
|
||||||
|
|
||||||
|
sealed class TorrentAddon : Addon() {
|
||||||
|
data class Installed(
|
||||||
|
override val name: String,
|
||||||
|
override val pkgName: String,
|
||||||
|
override val versionName: String,
|
||||||
|
override val versionCode: Long,
|
||||||
|
val extension: TorrentAddonApi,
|
||||||
|
val icon: Drawable?,
|
||||||
|
val hasUpdate: Boolean = false,
|
||||||
|
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ani.dantotsu.addons.torrent
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
|
||||||
|
|
||||||
|
interface TorrentAddonApi {
|
||||||
|
|
||||||
|
fun startServer(path: String)
|
||||||
|
|
||||||
|
fun stopServer()
|
||||||
|
|
||||||
|
fun echo(): String
|
||||||
|
|
||||||
|
fun removeTorrent(torrent: String)
|
||||||
|
|
||||||
|
fun addTorrent(
|
||||||
|
link: String,
|
||||||
|
title: String,
|
||||||
|
poster: String,
|
||||||
|
data: String,
|
||||||
|
save: Boolean,
|
||||||
|
): Torrent
|
||||||
|
|
||||||
|
fun getLink(torrent: Torrent, index: Int): String
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package ani.dantotsu.addons.torrent
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
|
||||||
|
import ani.dantotsu.addons.AddonListener
|
||||||
|
import ani.dantotsu.addons.AddonLoader
|
||||||
|
import ani.dantotsu.addons.AddonManager
|
||||||
|
import ani.dantotsu.addons.LoadResult
|
||||||
|
import ani.dantotsu.addons.AddonInstallReceiver
|
||||||
|
import ani.dantotsu.media.AddonType
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class TorrentAddonManager(
|
||||||
|
private val context: Context
|
||||||
|
) : AddonManager<TorrentAddon.Installed>(context) {
|
||||||
|
override var extension: TorrentAddon.Installed? = null
|
||||||
|
override var name: String = "Torrent Addon"
|
||||||
|
override var type: AddonType = AddonType.TORRENT
|
||||||
|
var torrentHash: String? = null
|
||||||
|
|
||||||
|
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||||
|
|
||||||
|
private var error: String? = null
|
||||||
|
|
||||||
|
override suspend fun init() {
|
||||||
|
extension = null
|
||||||
|
error = null
|
||||||
|
hasUpdate = false
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_isInitialized.value = false
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < 23) {
|
||||||
|
Logger.log("Torrent extension is not supported on this device.")
|
||||||
|
error = context.getString(R.string.torrent_extension_not_supported)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonInstallReceiver()
|
||||||
|
.setListener(InstallationListener(), type)
|
||||||
|
.register(context)
|
||||||
|
try {
|
||||||
|
val result = AddonLoader.loadExtension(
|
||||||
|
context,
|
||||||
|
TORRENT_PACKAGE,
|
||||||
|
TORRENT_CLASS,
|
||||||
|
type
|
||||||
|
) as TorrentLoadResult?
|
||||||
|
result?.let {
|
||||||
|
if (it is TorrentLoadResult.Success) {
|
||||||
|
extension = it.extension
|
||||||
|
hasUpdate = hasUpdate(REPO, it.extension.versionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_isInitialized.value = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.log("Error initializing torrent extension")
|
||||||
|
Logger.log(e)
|
||||||
|
error = e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return extension?.extension != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVersion(): String? {
|
||||||
|
return extension?.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPackageName(): String? {
|
||||||
|
return extension?.pkgName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hadError(context: Context): String? {
|
||||||
|
return if (isInitialized.value == true) {
|
||||||
|
if (error != null) {
|
||||||
|
error
|
||||||
|
} else if (extension != null) {
|
||||||
|
context.getString(R.string.loaded_successfully)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class InstallationListener : AddonListener {
|
||||||
|
override fun onAddonInstalled(result: LoadResult?) {
|
||||||
|
if (result is TorrentLoadResult.Success) {
|
||||||
|
extension = result.extension
|
||||||
|
hasUpdate = false
|
||||||
|
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddonUpdated(result: LoadResult?) {
|
||||||
|
if (result is TorrentLoadResult.Success) {
|
||||||
|
extension = result.extension
|
||||||
|
hasUpdate = false
|
||||||
|
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddonUninstalled(pkgName: String) {
|
||||||
|
if (pkgName == TORRENT_PACKAGE) {
|
||||||
|
extension = null
|
||||||
|
hasUpdate = false
|
||||||
|
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||||
|
installer.updateInstallStep(id, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setInstalling(id: Long) {
|
||||||
|
installer.updateInstallStep(id, InstallStep.Installing)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TORRENT_PACKAGE = "dantotsu.torrentAddon"
|
||||||
|
const val TORRENT_CLASS = "ani.dantotsu.torrentAddon.TorrentAddon"
|
||||||
|
const val REPO = "rebelonion/Dantotsu-Torrent-Addon"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ani.dantotsu.addons.torrent
|
||||||
|
|
||||||
|
import ani.dantotsu.addons.LoadResult
|
||||||
|
|
||||||
|
open class TorrentLoadResult : LoadResult() {
|
||||||
|
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
|
||||||
|
}
|
||||||
168
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
168
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package ani.dantotsu.addons.torrent
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_TORRENT_SERVER
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications.ID_TORRENT_SERVER
|
||||||
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
|
||||||
|
class ServerService : Service() {
|
||||||
|
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
|
||||||
|
private val applicationContext = Injekt.get<Application>()
|
||||||
|
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(
|
||||||
|
intent: Intent?,
|
||||||
|
flags: Int,
|
||||||
|
startId: Int,
|
||||||
|
): Int {
|
||||||
|
intent?.let {
|
||||||
|
if (it.action != null) {
|
||||||
|
when (it.action) {
|
||||||
|
ACTION_START -> {
|
||||||
|
startServer()
|
||||||
|
notification(applicationContext)
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_STOP -> {
|
||||||
|
stopServer()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startServer() {
|
||||||
|
serviceScope.launch {
|
||||||
|
val echo = extension.echo()
|
||||||
|
if (echo == "") {
|
||||||
|
extension.startServer(filesDir.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopServer() {
|
||||||
|
serviceScope.launch {
|
||||||
|
extension.stopServer()
|
||||||
|
applicationContext.cancelNotification(ID_TORRENT_SERVER)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notification(context: Context) {
|
||||||
|
val exitPendingIntent =
|
||||||
|
PendingIntent.getService(
|
||||||
|
applicationContext,
|
||||||
|
0,
|
||||||
|
Intent(applicationContext, ServerService::class.java).apply {
|
||||||
|
action = ACTION_STOP
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
val builder = context.notificationBuilder(CHANNEL_TORRENT_SERVER) {
|
||||||
|
setSmallIcon(R.drawable.notification_icon)
|
||||||
|
setContentText("Torrent Server")
|
||||||
|
setContentTitle("Server is running…")
|
||||||
|
setAutoCancel(false)
|
||||||
|
setOngoing(true)
|
||||||
|
setUsesChronometer(true)
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_circle_cancel,
|
||||||
|
"Stop",
|
||||||
|
exitPendingIntent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
ID_TORRENT_SERVER,
|
||||||
|
builder.build(),
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(ID_TORRENT_SERVER, builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_START = "start_torrent_server"
|
||||||
|
const val ACTION_STOP = "stop_torrent_server"
|
||||||
|
|
||||||
|
fun isRunning(): Boolean {
|
||||||
|
with(Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
|
||||||
|
@Suppress("DEPRECATION") // We only need our services
|
||||||
|
getRunningServices(Int.MAX_VALUE).forEach {
|
||||||
|
if (ServerService::class.java.name.equals(it.service.className)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
try {
|
||||||
|
val intent =
|
||||||
|
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||||
|
action = ACTION_START
|
||||||
|
}
|
||||||
|
Injekt.get<Application>().startService(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
try {
|
||||||
|
val intent =
|
||||||
|
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||||
|
action = ACTION_STOP
|
||||||
|
}
|
||||||
|
Injekt.get<Application>().startService(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wait(timeout: Int = -1): Boolean {
|
||||||
|
var count = 0
|
||||||
|
if (timeout < 0) {
|
||||||
|
count = -20
|
||||||
|
}
|
||||||
|
var echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||||
|
while (echo == "") {
|
||||||
|
Thread.sleep(1000)
|
||||||
|
count++
|
||||||
|
if (count > timeout) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||||
|
}
|
||||||
|
Logger.log("ServerService: Server started: $echo")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import androidx.annotation.OptIn
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.database.StandaloneDatabaseProvider
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
|
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
@@ -18,6 +20,7 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
|||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||||
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||||
@@ -29,6 +32,7 @@ import uy.kohesive.injekt.api.addSingletonFactory
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
@kotlin.OptIn(ExperimentalSerializationApi::class)
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
@@ -36,10 +40,13 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory { DownloadsManager(app) }
|
addSingletonFactory { DownloadsManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { NetworkHelper(app) }
|
addSingletonFactory { NetworkHelper(app) }
|
||||||
|
addSingletonFactory { NetworkHelper(app).client }
|
||||||
|
|
||||||
addSingletonFactory { AnimeExtensionManager(app) }
|
addSingletonFactory { AnimeExtensionManager(app) }
|
||||||
addSingletonFactory { MangaExtensionManager(app) }
|
addSingletonFactory { MangaExtensionManager(app) }
|
||||||
addSingletonFactory { NovelExtensionManager(app) }
|
addSingletonFactory { NovelExtensionManager(app) }
|
||||||
|
addSingletonFactory { TorrentAddonManager(app) }
|
||||||
|
addSingletonFactory { DownloadAddonManager(app) }
|
||||||
|
|
||||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ package ani.dantotsu.connections.anilist
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.client
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.connections.comments.CommentsAPI
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
import ani.dantotsu.tryWithSuspend
|
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
@@ -40,20 +39,54 @@ object Anilist {
|
|||||||
"SCORE_DESC",
|
"SCORE_DESC",
|
||||||
"POPULARITY_DESC",
|
"POPULARITY_DESC",
|
||||||
"TRENDING_DESC",
|
"TRENDING_DESC",
|
||||||
|
"START_DATE_DESC",
|
||||||
"TITLE_ENGLISH",
|
"TITLE_ENGLISH",
|
||||||
"TITLE_ENGLISH_DESC",
|
"TITLE_ENGLISH_DESC",
|
||||||
"SCORE"
|
"SCORE"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val source = listOf(
|
||||||
|
"ORIGINAL",
|
||||||
|
"MANGA",
|
||||||
|
"LIGHT NOVEL",
|
||||||
|
"VISUAL NOVEL",
|
||||||
|
"VIDEO GAME",
|
||||||
|
"OTHER",
|
||||||
|
"NOVEL",
|
||||||
|
"DOUJINSHI",
|
||||||
|
"ANIME",
|
||||||
|
"WEB NOVEL",
|
||||||
|
"LIVE ACTION",
|
||||||
|
"GAME",
|
||||||
|
"COMIC",
|
||||||
|
"MULTIMEDIA PROJECT",
|
||||||
|
"PICTURE BOOK"
|
||||||
|
)
|
||||||
|
|
||||||
|
val animeStatus = listOf(
|
||||||
|
"FINISHED",
|
||||||
|
"RELEASING",
|
||||||
|
"NOT YET RELEASED",
|
||||||
|
"CANCELLED"
|
||||||
|
)
|
||||||
|
|
||||||
|
val mangaStatus = listOf(
|
||||||
|
"FINISHED",
|
||||||
|
"RELEASING",
|
||||||
|
"NOT YET RELEASED",
|
||||||
|
"HIATUS",
|
||||||
|
"CANCELLED"
|
||||||
|
)
|
||||||
|
|
||||||
val seasons = listOf(
|
val seasons = listOf(
|
||||||
"WINTER", "SPRING", "SUMMER", "FALL"
|
"WINTER", "SPRING", "SUMMER", "FALL"
|
||||||
)
|
)
|
||||||
|
|
||||||
val anime_formats = listOf(
|
val animeFormats = listOf(
|
||||||
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
||||||
)
|
)
|
||||||
|
|
||||||
val manga_formats = listOf(
|
val mangaFormats = listOf(
|
||||||
"MANGA", "NOVEL", "ONE SHOT"
|
"MANGA", "NOVEL", "ONE SHOT"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,6 +150,9 @@ object Anilist {
|
|||||||
episodesWatched = null
|
episodesWatched = null
|
||||||
chapterRead = null
|
chapterRead = null
|
||||||
PrefManager.removeVal(PrefName.AnilistToken)
|
PrefManager.removeVal(PrefName.AnilistToken)
|
||||||
|
//logout from comments api
|
||||||
|
CommentsAPI.logout()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> executeQuery(
|
suspend inline fun <reified T : Any> executeQuery(
|
||||||
@@ -163,7 +199,9 @@ object Anilist {
|
|||||||
toast("Rate limited. Try after $retry seconds")
|
toast("Rate limited. Try after $retry seconds")
|
||||||
throw Exception("Rate limited after $retry seconds")
|
throw Exception("Rate limited after $retry seconds")
|
||||||
}
|
}
|
||||||
if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))}
|
if (!json.text.startsWith("{")) {
|
||||||
|
throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||||
|
}
|
||||||
if (show) Logger.log("Anilist Response: ${json.text}")
|
if (show) Logger.log("Anilist Response: ${json.text}")
|
||||||
json.parsed()
|
json.parsed()
|
||||||
} else null
|
} else null
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import ani.dantotsu.media.Character
|
|||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.Studio
|
import ani.dantotsu.media.Studio
|
||||||
import ani.dantotsu.others.MalScraper
|
import ani.dantotsu.others.MalScraper
|
||||||
|
import ani.dantotsu.profile.User
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
@@ -72,18 +73,19 @@ class AnilistQueries {
|
|||||||
media.cameFromContinue = false
|
media.cameFromContinue = false
|
||||||
|
|
||||||
val query =
|
val query =
|
||||||
"""{Media(id:${media.id}){id mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
|
"""{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val anilist = async {
|
val anilist = async {
|
||||||
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
fun parse() {
|
fun parse() {
|
||||||
val fetchedMedia = response?.data?.media ?: return
|
val fetchedMedia = response?.data?.media ?: return
|
||||||
|
val user = response?.data?.page
|
||||||
media.source = fetchedMedia.source?.toString()
|
media.source = fetchedMedia.source?.toString()
|
||||||
media.countryOfOrigin = fetchedMedia.countryOfOrigin
|
media.countryOfOrigin = fetchedMedia.countryOfOrigin
|
||||||
media.format = fetchedMedia.format?.toString()
|
media.format = fetchedMedia.format?.toString()
|
||||||
|
media.favourites = fetchedMedia.favourites
|
||||||
|
media.popularity = fetchedMedia.popularity
|
||||||
media.startDate = fetchedMedia.startDate
|
media.startDate = fetchedMedia.startDate
|
||||||
media.endDate = fetchedMedia.endDate
|
media.endDate = fetchedMedia.endDate
|
||||||
|
|
||||||
@@ -138,7 +140,15 @@ class AnilistQueries {
|
|||||||
?: "SUPPORTING"
|
?: "SUPPORTING"
|
||||||
|
|
||||||
else -> i.role.toString()
|
else -> i.role.toString()
|
||||||
}
|
},
|
||||||
|
voiceActor = i.voiceActors?.map {
|
||||||
|
Author(
|
||||||
|
id = it.id,
|
||||||
|
name = it.name?.userPreferred,
|
||||||
|
image = it.image?.large,
|
||||||
|
role = it.languageV2
|
||||||
|
)
|
||||||
|
} as ArrayList<Author>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,7 +162,7 @@ class AnilistQueries {
|
|||||||
Author(
|
Author(
|
||||||
id = id,
|
id = id,
|
||||||
name = i.node?.name?.userPreferred,
|
name = i.node?.name?.userPreferred,
|
||||||
image = i.node?.image?.medium,
|
image = i.node?.image?.large,
|
||||||
role = when (i.role.toString()) {
|
role = when (i.role.toString()) {
|
||||||
"MAIN" -> currContext()?.getString(R.string.main_role)
|
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||||
?: "MAIN"
|
?: "MAIN"
|
||||||
@@ -199,7 +209,24 @@ class AnilistQueries {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (user?.mediaList?.isNotEmpty() == true) {
|
||||||
|
media.users = user.mediaList?.mapNotNull {
|
||||||
|
it.user?.let { user ->
|
||||||
|
if (user.id != Anilist.userid) {
|
||||||
|
User(
|
||||||
|
user.id,
|
||||||
|
user.name ?: "Unknown",
|
||||||
|
user.avatar?.large,
|
||||||
|
"",
|
||||||
|
it.status?.toString(),
|
||||||
|
it.score,
|
||||||
|
it.progress,
|
||||||
|
fetchedMedia.episodes ?: fetchedMedia.chapters,
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}?.toCollection(arrayListOf()) ?: arrayListOf()
|
||||||
|
}
|
||||||
if (fetchedMedia.mediaListEntry != null) {
|
if (fetchedMedia.mediaListEntry != null) {
|
||||||
fetchedMedia.mediaListEntry?.apply {
|
fetchedMedia.mediaListEntry?.apply {
|
||||||
media.userProgress = progress
|
media.userProgress = progress
|
||||||
@@ -386,6 +413,7 @@ class AnilistQueries {
|
|||||||
returnArray.addAll(map.values)
|
returnArray.addAll(map.values)
|
||||||
return returnArray
|
return returnArray
|
||||||
}
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val list = PrefManager.getNullableCustomVal(
|
val list = PrefManager.getNullableCustomVal(
|
||||||
"continueAnimeList",
|
"continueAnimeList",
|
||||||
listOf<Int>(),
|
listOf<Int>(),
|
||||||
@@ -543,6 +571,7 @@ class AnilistQueries {
|
|||||||
returnMap["current$type"] = returnArray
|
returnMap["current$type"] = returnArray
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val list = PrefManager.getNullableCustomVal(
|
val list = PrefManager.getNullableCustomVal(
|
||||||
"continueAnimeList",
|
"continueAnimeList",
|
||||||
listOf<Int>(),
|
listOf<Int>(),
|
||||||
@@ -572,6 +601,7 @@ class AnilistQueries {
|
|||||||
subMap[m.id] = m
|
subMap[m.id] = m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val list = PrefManager.getNullableCustomVal(
|
val list = PrefManager.getNullableCustomVal(
|
||||||
"continueAnimeList",
|
"continueAnimeList",
|
||||||
listOf<Int>(),
|
listOf<Int>(),
|
||||||
@@ -733,7 +763,7 @@ class AnilistQueries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sorted["All"] = all
|
sorted["All"] = all
|
||||||
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
val listSort: String? = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
||||||
else PrefManager.getVal(PrefName.MangaListSortOrder)
|
else PrefManager.getVal(PrefName.MangaListSortOrder)
|
||||||
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
||||||
for (i in sorted.keys) {
|
for (i in sorted.keys) {
|
||||||
@@ -881,18 +911,23 @@ class AnilistQueries {
|
|||||||
sort: String? = null,
|
sort: String? = null,
|
||||||
genres: MutableList<String>? = null,
|
genres: MutableList<String>? = null,
|
||||||
tags: MutableList<String>? = null,
|
tags: MutableList<String>? = null,
|
||||||
|
status: String? = null,
|
||||||
|
source: String? = null,
|
||||||
format: String? = null,
|
format: String? = null,
|
||||||
|
countryOfOrigin: String? = null,
|
||||||
isAdult: Boolean = false,
|
isAdult: Boolean = false,
|
||||||
onList: Boolean? = null,
|
onList: Boolean? = null,
|
||||||
excludedGenres: MutableList<String>? = null,
|
excludedGenres: MutableList<String>? = null,
|
||||||
excludedTags: MutableList<String>? = null,
|
excludedTags: MutableList<String>? = null,
|
||||||
|
startYear: Int? = null,
|
||||||
seasonYear: Int? = null,
|
seasonYear: Int? = null,
|
||||||
season: String? = null,
|
season: String? = null,
|
||||||
id: Int? = null,
|
id: Int? = null,
|
||||||
hd: Boolean = false,
|
hd: Boolean = false,
|
||||||
|
adultOnly: Boolean = false
|
||||||
): SearchResults? {
|
): SearchResults? {
|
||||||
val query = """
|
val query = """
|
||||||
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
|
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
|
||||||
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
|
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
total
|
total
|
||||||
@@ -937,14 +972,19 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
}
|
}
|
||||||
""".replace("\n", " ").replace(""" """, "")
|
""".replace("\n", " ").replace(""" """, "")
|
||||||
val variables = """{"type":"$type","isAdult":$isAdult
|
val variables = """{"type":"$type","isAdult":$isAdult
|
||||||
|
${if (adultOnly) ""","isAdult":true""" else ""}
|
||||||
${if (onList != null) ""","onList":$onList""" else ""}
|
${if (onList != null) ""","onList":$onList""" else ""}
|
||||||
${if (page != null) ""","page":"$page"""" else ""}
|
${if (page != null) ""","page":"$page"""" else ""}
|
||||||
${if (id != null) ""","id":"$id"""" else ""}
|
${if (id != null) ""","id":"$id"""" else ""}
|
||||||
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
${if (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||||
|
${if (type == "MANGA" && startYear != null) ""","yearGreater":${startYear}0000,"yearLesser":${startYear + 1}0000""" else ""}
|
||||||
${if (season != null) ""","season":"$season"""" else ""}
|
${if (season != null) ""","season":"$season"""" else ""}
|
||||||
${if (search != null) ""","search":"$search"""" else ""}
|
${if (search != null) ""","search":"$search"""" else ""}
|
||||||
|
${if (source != null) ""","source":"$source"""" else ""}
|
||||||
${if (sort != null) ""","sort":"$sort"""" else ""}
|
${if (sort != null) ""","sort":"$sort"""" else ""}
|
||||||
|
${if (status != null) ""","status":"$status"""" else ""}
|
||||||
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
||||||
|
${if (countryOfOrigin != null) ""","countryOfOrigin":"$countryOfOrigin"""" else ""}
|
||||||
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
||||||
${
|
${
|
||||||
if (excludedGenres?.isNotEmpty() == true)
|
if (excludedGenres?.isNotEmpty() == true)
|
||||||
@@ -976,7 +1016,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
else ""
|
else ""
|
||||||
}
|
}
|
||||||
}""".replace("\n", " ").replace(""" """, "")
|
}""".replace("\n", " ").replace(""" """, "")
|
||||||
|
|
||||||
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
|
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
|
||||||
if (response?.media != null) {
|
if (response?.media != null) {
|
||||||
val responseArray = arrayListOf<Media>()
|
val responseArray = arrayListOf<Media>()
|
||||||
@@ -1008,7 +1047,11 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
excludedGenres = excludedGenres,
|
excludedGenres = excludedGenres,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
excludedTags = excludedTags,
|
excludedTags = excludedTags,
|
||||||
|
status = status,
|
||||||
|
source = source,
|
||||||
format = format,
|
format = format,
|
||||||
|
countryOfOrigin = countryOfOrigin,
|
||||||
|
startYear = startYear,
|
||||||
seasonYear = seasonYear,
|
seasonYear = seasonYear,
|
||||||
season = season,
|
season = season,
|
||||||
results = responseArray,
|
results = responseArray,
|
||||||
@@ -1019,11 +1062,156 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val onListAnime =
|
||||||
|
(if (PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace(
|
||||||
|
"\"",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
private val isAdult =
|
||||||
|
(if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
|
||||||
|
|
||||||
|
private fun recentAnimeUpdates(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trendingMovies(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun topRatedAnime(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mostFavAnime(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadAnimeList(): Map<String, ArrayList<Media>> {
|
||||||
|
val list = mutableMapOf<String, ArrayList<Media>>()
|
||||||
|
fun query(): String {
|
||||||
|
return """{
|
||||||
|
recentUpdates:${recentAnimeUpdates(1)}
|
||||||
|
recentUpdates2:${recentAnimeUpdates(2)}
|
||||||
|
trendingMovies:${trendingMovies(1)}
|
||||||
|
trendingMovies2:${trendingMovies(2)}
|
||||||
|
topRated:${topRatedAnime(1)}
|
||||||
|
topRated2:${topRatedAnime(2)}
|
||||||
|
mostFav:${mostFavAnime(1)}
|
||||||
|
mostFav2:${mostFavAnime(2)}
|
||||||
|
}""".trimIndent()
|
||||||
|
}
|
||||||
|
executeQuery<Query.AnimeList>(query(), force = true)?.data?.apply {
|
||||||
|
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||||
|
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
|
||||||
|
val idArr = mutableListOf<Int>()
|
||||||
|
list["recentUpdates"] = recentUpdates?.airingSchedules?.mapNotNull { i ->
|
||||||
|
i.media?.let {
|
||||||
|
if (!idArr.contains(it.id))
|
||||||
|
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
|
||||||
|
idArr.add(it.id)
|
||||||
|
Media(it)
|
||||||
|
} else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
|
||||||
|
idArr.add(it.id)
|
||||||
|
Media(it)
|
||||||
|
} else if ((listOnly && it.mediaListEntry != null)) {
|
||||||
|
idArr.add(it.id)
|
||||||
|
Media(it)
|
||||||
|
} else null
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
} as ArrayList<Media>
|
||||||
|
|
||||||
|
list["trendingMovies"] = trendingMovies?.media?.map { Media(it) } as ArrayList<Media>
|
||||||
|
list["topRated"] = topRated?.media?.map { Media(it) } as ArrayList<Media>
|
||||||
|
list["mostFav"] = mostFav?.media?.map { Media(it) } as ArrayList<Media>
|
||||||
|
|
||||||
|
list["recentUpdates"]?.addAll(recentUpdates2?.airingSchedules?.mapNotNull { i ->
|
||||||
|
i.media?.let {
|
||||||
|
if (!idArr.contains(it.id))
|
||||||
|
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
|
||||||
|
idArr.add(it.id)
|
||||||
|
Media(it)
|
||||||
|
} else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
|
||||||
|
idArr.add(it.id)
|
||||||
|
Media(it)
|
||||||
|
} else if ((listOnly && it.mediaListEntry != null)) {
|
||||||
|
idArr.add(it.id)
|
||||||
|
Media(it)
|
||||||
|
} else null
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
} as ArrayList<Media>)
|
||||||
|
list["trendingMovies"]?.addAll(trendingMovies2?.media?.map { Media(it) } as ArrayList<Media>)
|
||||||
|
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) } as ArrayList<Media>)
|
||||||
|
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) } as ArrayList<Media>)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
private val onListManga =
|
||||||
|
(if (PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace(
|
||||||
|
"\"",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun trendingManga(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trendingManhwa(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trendingNovel(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun topRatedManga(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mostFavManga(page: Int): String {
|
||||||
|
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadMangaList(): Map<String, ArrayList<Media>> {
|
||||||
|
val list = mutableMapOf<String, ArrayList<Media>>()
|
||||||
|
fun query(): String {
|
||||||
|
return """{
|
||||||
|
trendingManga:${trendingManga(1)}
|
||||||
|
trendingManga2:${trendingManga(2)}
|
||||||
|
trendingManhwa:${trendingManhwa(1)}
|
||||||
|
trendingManhwa2:${trendingManhwa(2)}
|
||||||
|
trendingNovel:${trendingNovel(1)}
|
||||||
|
trendingNovel2:${trendingNovel(2)}
|
||||||
|
topRated:${topRatedManga(1)}
|
||||||
|
topRated2:${topRatedManga(2)}
|
||||||
|
mostFav:${mostFavManga(1)}
|
||||||
|
mostFav2:${mostFavManga(2)}
|
||||||
|
}""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
executeQuery<Query.MangaList>(query(), force = true)?.data?.apply {
|
||||||
|
list["trendingManga"] = trendingManga?.media?.map { Media(it) } as ArrayList<Media>
|
||||||
|
list["trendingManhwa"] = trendingManhwa?.media?.map { Media(it) } as ArrayList<Media>
|
||||||
|
list["trendingNovel"] = trendingNovel?.media?.map { Media(it) } as ArrayList<Media>
|
||||||
|
list["topRated"] = topRated?.media?.map { Media(it) } as ArrayList<Media>
|
||||||
|
list["mostFav"] = mostFav?.media?.map { Media(it) } as ArrayList<Media>
|
||||||
|
list["trendingManga"]?.addAll(trendingManga2?.media?.map { Media(it) } as ArrayList<Media>)
|
||||||
|
list["trendingManhwa"]?.addAll(trendingManhwa2?.media?.map { Media(it) } as ArrayList<Media>)
|
||||||
|
list["trendingNovel"]?.addAll(trendingNovel2?.media?.map { Media(it) } as ArrayList<Media>)
|
||||||
|
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) } as ArrayList<Media>)
|
||||||
|
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) } as ArrayList<Media>)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun recentlyUpdated(
|
suspend fun recentlyUpdated(
|
||||||
smaller: Boolean = true,
|
|
||||||
greater: Long = 0,
|
greater: Long = 0,
|
||||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||||
): MutableList<Media>? {
|
): MutableList<Media> {
|
||||||
suspend fun execute(page: Int = 1): Page? {
|
suspend fun execute(page: Int = 1): Page? {
|
||||||
val query = """{
|
val query = """{
|
||||||
Page(page:$page,perPage:50) {
|
Page(page:$page,perPage:50) {
|
||||||
@@ -1070,41 +1258,26 @@ Page(page:$page,perPage:50) {
|
|||||||
}""".replace("\n", " ").replace(""" """, "")
|
}""".replace("\n", " ").replace(""" """, "")
|
||||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||||
}
|
}
|
||||||
if (smaller) {
|
|
||||||
val response = execute()?.airingSchedules ?: return null
|
var i = 1
|
||||||
val idArr = mutableListOf<Int>()
|
val list = mutableListOf<Media>()
|
||||||
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
var res: Page? = null
|
||||||
return response.mapNotNull { i ->
|
suspend fun next() {
|
||||||
i.media?.let {
|
res = execute(i)
|
||||||
if (!idArr.contains(it.id))
|
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
||||||
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
|
j.media?.let {
|
||||||
idArr.add(it.id)
|
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
|
||||||
Media(it)
|
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
||||||
} else null
|
} else null
|
||||||
else null
|
|
||||||
}
|
}
|
||||||
}.toMutableList()
|
} ?: listOf())
|
||||||
} else {
|
|
||||||
var i = 1
|
|
||||||
val list = mutableListOf<Media>()
|
|
||||||
var res: Page? = null
|
|
||||||
suspend fun next() {
|
|
||||||
res = execute(i)
|
|
||||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
|
||||||
j.media?.let {
|
|
||||||
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
|
|
||||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
} ?: listOf())
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
while (res?.pageInfo?.hasNextPage == true) {
|
|
||||||
next()
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return list.reversed().toMutableList()
|
|
||||||
}
|
}
|
||||||
|
next()
|
||||||
|
while (res?.pageInfo?.hasNextPage == true) {
|
||||||
|
next()
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return list.reversed().toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCharacterDetails(character: Character): Character {
|
suspend fun getCharacterDetails(character: Character): Character {
|
||||||
@@ -1290,19 +1463,39 @@ Page(page:$page,perPage:50) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
characters(page: $page,sort:FAVOURITES_DESC) {
|
||||||
|
pageInfo{
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
nodes{
|
||||||
|
id
|
||||||
|
name {
|
||||||
|
first
|
||||||
|
middle
|
||||||
|
last
|
||||||
|
full
|
||||||
|
native
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
image {
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}""".replace("\n", " ").replace(""" """, "")
|
}""".replace("\n", " ").replace(""" """, "")
|
||||||
|
|
||||||
var hasNextPage = true
|
var hasNextPage = true
|
||||||
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
|
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
|
||||||
var page = 0
|
var page = 0
|
||||||
|
val characters = arrayListOf<Character>()
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
page++
|
page++
|
||||||
hasNextPage = executeQuery<Query.Author>(
|
val query = executeQuery<Query.Author>(
|
||||||
query(page),
|
query(page), force = true
|
||||||
force = true
|
)?.data?.author
|
||||||
)?.data?.author?.staffMedia?.let {
|
hasNextPage = query?.staffMedia?.let {
|
||||||
it.edges?.forEach { i ->
|
it.edges?.forEach { i ->
|
||||||
i.node?.apply {
|
i.node?.apply {
|
||||||
val status = status.toString()
|
val status = status.toString()
|
||||||
@@ -1317,6 +1510,20 @@ Page(page:$page,perPage:50) {
|
|||||||
}
|
}
|
||||||
it.pageInfo?.hasNextPage == true
|
it.pageInfo?.hasNextPage == true
|
||||||
} ?: false
|
} ?: false
|
||||||
|
query?.characters?.let {
|
||||||
|
it.nodes?.forEach { i ->
|
||||||
|
characters.add(
|
||||||
|
Character(
|
||||||
|
i.id,
|
||||||
|
i.name?.userPreferred,
|
||||||
|
i.image?.large,
|
||||||
|
i.image?.medium,
|
||||||
|
"",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yearMedia.contains("CANCELLED")) {
|
if (yearMedia.contains("CANCELLED")) {
|
||||||
@@ -1324,6 +1531,7 @@ Page(page:$page,perPage:50) {
|
|||||||
yearMedia.remove("CANCELLED")
|
yearMedia.remove("CANCELLED")
|
||||||
yearMedia["CANCELLED"] = a
|
yearMedia["CANCELLED"] = a
|
||||||
}
|
}
|
||||||
|
author.character = characters
|
||||||
author.yearMedia = yearMedia
|
author.yearMedia = yearMedia
|
||||||
return author
|
return author
|
||||||
}
|
}
|
||||||
@@ -1390,17 +1598,16 @@ Page(page:$page,perPage:50) {
|
|||||||
"""{
|
"""{
|
||||||
favoriteAnime:${userFavMediaQuery(true, 1, id)}
|
favoriteAnime:${userFavMediaQuery(true, 1, id)}
|
||||||
favoriteManga:${userFavMediaQuery(false, 1, id)}
|
favoriteManga:${userFavMediaQuery(false, 1, id)}
|
||||||
animeMediaList:${bannerImageQuery("ANIME", id)}
|
|
||||||
mangaMediaList:${bannerImageQuery("MANGA", id)}
|
|
||||||
}""".trimIndent(), force = true
|
}""".trimIndent(), force = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bannerImageQuery(type: String, id: Int?): String {
|
|
||||||
return """MediaListCollection(userId: ${id}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } }"""
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? {
|
suspend fun getNotifications(
|
||||||
|
id: Int,
|
||||||
|
page: Int = 1,
|
||||||
|
resetNotification: Boolean = true
|
||||||
|
): NotificationResponse? {
|
||||||
val reset = if (resetNotification) "true" else "false"
|
val reset = if (resetNotification) "true" else "false"
|
||||||
val res = executeQuery<NotificationResponse>(
|
val res = executeQuery<NotificationResponse>(
|
||||||
"""{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""",
|
"""{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""",
|
||||||
@@ -1415,7 +1622,12 @@ Page(page:$page,perPage:50) {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getFeed(userId: Int?, global: Boolean = false, page: Int = 1, activityId: Int? = null): FeedResponse? {
|
suspend fun getFeed(
|
||||||
|
userId: Int?,
|
||||||
|
global: Boolean = false,
|
||||||
|
page: Int = 1,
|
||||||
|
activityId: Int? = null
|
||||||
|
): FeedResponse? {
|
||||||
val filter = if (activityId != null) "id:$activityId,"
|
val filter = if (activityId != null) "id:$activityId,"
|
||||||
else if (userId != null) "userId:$userId,"
|
else if (userId != null) "userId:$userId,"
|
||||||
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
|
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
|
||||||
@@ -1426,14 +1638,44 @@ Page(page:$page,perPage:50) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isUserFav(favType: AnilistMutations.FavType, id: Int): Boolean { //anilist isFavourite is broken, so we need to check it manually
|
suspend fun getUpcomingAnime(id: String): List<Media> {
|
||||||
val res = getUserProfile(Anilist.userid?: return false)
|
val res = executeQuery<Query.MediaListCollection>(
|
||||||
|
"""{MediaListCollection(userId:$id,type:ANIME){lists{name entries{media{id,isFavourite,title{userPreferred,romaji}coverImage{medium}nextAiringEpisode{timeUntilAiring}}}}}}""",
|
||||||
|
force = true
|
||||||
|
)
|
||||||
|
val list = mutableListOf<Media>()
|
||||||
|
res?.data?.mediaListCollection?.lists?.forEach { listEntry ->
|
||||||
|
listEntry.entries?.forEach { entry ->
|
||||||
|
entry.media?.nextAiringEpisode?.timeUntilAiring?.let {
|
||||||
|
list.add(Media(entry.media!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list.sortedBy { it.timeUntilAiring }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
.filter { it.timeUntilAiring != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun isUserFav(
|
||||||
|
favType: AnilistMutations.FavType,
|
||||||
|
id: Int
|
||||||
|
): Boolean { //anilist isFavourite is broken, so we need to check it manually
|
||||||
|
val res = getUserProfile(Anilist.userid ?: return false)
|
||||||
return when (favType) {
|
return when (favType) {
|
||||||
AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id } ?: false
|
AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id }
|
||||||
AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id } ?: false
|
?: false
|
||||||
AnilistMutations.FavType.CHARACTER -> res?.data?.user?.favourites?.characters?.nodes?.any { it.id == id } ?: false
|
|
||||||
AnilistMutations.FavType.STAFF -> res?.data?.user?.favourites?.staff?.nodes?.any { it.id == id } ?: false
|
AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id }
|
||||||
AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id } ?: false
|
?: false
|
||||||
|
|
||||||
|
AnilistMutations.FavType.CHARACTER -> res?.data?.user?.favourites?.characters?.nodes?.any { it.id == id }
|
||||||
|
?: false
|
||||||
|
|
||||||
|
AnilistMutations.FavType.STAFF -> res?.data?.user?.favourites?.staff?.nodes?.any { it.id == id }
|
||||||
|
?: false
|
||||||
|
|
||||||
|
AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id }
|
||||||
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,45 +58,36 @@ class AnilistHomeViewModel : ViewModel() {
|
|||||||
MutableLiveData<ArrayList<Media>>(null)
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
||||||
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
|
||||||
|
|
||||||
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
||||||
MutableLiveData<ArrayList<Media>>(null)
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
|
||||||
|
|
||||||
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
||||||
MutableLiveData<ArrayList<Media>>(null)
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||||
suspend fun setAnimePlanned() =
|
|
||||||
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
|
||||||
|
|
||||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
|
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
|
||||||
MutableLiveData<ArrayList<Media>>(null)
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
||||||
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
|
||||||
|
|
||||||
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
||||||
MutableLiveData<ArrayList<Media>>(null)
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
|
||||||
|
|
||||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
||||||
MutableLiveData<ArrayList<Media>>(null)
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||||
suspend fun setMangaPlanned() =
|
|
||||||
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
|
||||||
|
|
||||||
private val recommendation: MutableLiveData<ArrayList<Media>> =
|
private val recommendation: MutableLiveData<ArrayList<Media>> =
|
||||||
MutableLiveData<ArrayList<Media>>(null)
|
MutableLiveData<ArrayList<Media>>(null)
|
||||||
|
|
||||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
|
||||||
|
|
||||||
suspend fun initHomePage() {
|
suspend fun initHomePage() {
|
||||||
val res = Anilist.query.initHomePage()
|
val res = Anilist.query.initHomePage()
|
||||||
@@ -112,8 +103,8 @@ class AnilistHomeViewModel : ViewModel() {
|
|||||||
|
|
||||||
suspend fun loadMain(context: FragmentActivity) {
|
suspend fun loadMain(context: FragmentActivity) {
|
||||||
Anilist.getSavedToken()
|
Anilist.getSavedToken()
|
||||||
MAL.getSavedToken(context)
|
MAL.getSavedToken()
|
||||||
Discord.getSavedToken(context)
|
Discord.getSavedToken()
|
||||||
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||||
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||||
}
|
}
|
||||||
@@ -144,22 +135,19 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||||||
sort = Anilist.sortBy[2],
|
sort = Anilist.sortBy[2],
|
||||||
season = season,
|
season = season,
|
||||||
seasonYear = year,
|
seasonYear = year,
|
||||||
hd = true
|
hd = true,
|
||||||
|
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||||
)?.results
|
)?.results
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val updated: MutableLiveData<MutableList<Media>> =
|
|
||||||
MutableLiveData<MutableList<Media>>(null)
|
|
||||||
|
|
||||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
|
||||||
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
|
||||||
|
|
||||||
private val animePopular = MutableLiveData<SearchResults?>(null)
|
private val animePopular = MutableLiveData<SearchResults?>(null)
|
||||||
|
|
||||||
fun getPopular(): LiveData<SearchResults?> = animePopular
|
fun getPopular(): LiveData<SearchResults?> = animePopular
|
||||||
suspend fun loadPopular(
|
suspend fun loadPopular(
|
||||||
type: String,
|
type: String,
|
||||||
search_val: String? = null,
|
searchVal: String? = null,
|
||||||
genres: ArrayList<String>? = null,
|
genres: ArrayList<String>? = null,
|
||||||
sort: String = Anilist.sortBy[1],
|
sort: String = Anilist.sortBy[1],
|
||||||
onList: Boolean = true,
|
onList: Boolean = true,
|
||||||
@@ -167,10 +155,11 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||||||
animePopular.postValue(
|
animePopular.postValue(
|
||||||
Anilist.query.search(
|
Anilist.query.search(
|
||||||
type,
|
type,
|
||||||
search = search_val,
|
search = searchVal,
|
||||||
onList = if (onList) null else false,
|
onList = if (onList) null else false,
|
||||||
sort = sort,
|
sort = sort,
|
||||||
genres = genres
|
genres = genres,
|
||||||
|
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -185,13 +174,43 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||||||
r.sort,
|
r.sort,
|
||||||
r.genres,
|
r.genres,
|
||||||
r.tags,
|
r.tags,
|
||||||
|
r.status,
|
||||||
|
r.source,
|
||||||
r.format,
|
r.format,
|
||||||
|
r.countryOfOrigin,
|
||||||
r.isAdult,
|
r.isAdult,
|
||||||
r.onList
|
r.onList,
|
||||||
|
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
var loaded: Boolean = false
|
var loaded: Boolean = false
|
||||||
|
private val updated: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||||
|
|
||||||
|
private val popularMovies: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getMovies(): LiveData<MutableList<Media>> = popularMovies
|
||||||
|
|
||||||
|
private val topRatedAnime: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getTopRated(): LiveData<MutableList<Media>> = topRatedAnime
|
||||||
|
|
||||||
|
private val mostFavAnime: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getMostFav(): LiveData<MutableList<Media>> = mostFavAnime
|
||||||
|
suspend fun loadAll() {
|
||||||
|
val list = Anilist.query.loadAnimeList()
|
||||||
|
updated.postValue(list["recentUpdates"])
|
||||||
|
popularMovies.postValue(list["trendingMovies"])
|
||||||
|
topRatedAnime.postValue(list["topRated"])
|
||||||
|
mostFavAnime.postValue(list["mostFav"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnilistMangaViewModel : ViewModel() {
|
class AnilistMangaViewModel : ViewModel() {
|
||||||
@@ -209,29 +228,17 @@ class AnilistMangaViewModel : ViewModel() {
|
|||||||
type,
|
type,
|
||||||
perPage = 10,
|
perPage = 10,
|
||||||
sort = Anilist.sortBy[2],
|
sort = Anilist.sortBy[2],
|
||||||
hd = true
|
hd = true,
|
||||||
|
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||||
)?.results
|
)?.results
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
||||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||||
suspend fun loadPopular(
|
suspend fun loadPopular(
|
||||||
type: String,
|
type: String,
|
||||||
search_val: String? = null,
|
searchVal: String? = null,
|
||||||
genres: ArrayList<String>? = null,
|
genres: ArrayList<String>? = null,
|
||||||
sort: String = Anilist.sortBy[1],
|
sort: String = Anilist.sortBy[1],
|
||||||
onList: Boolean = true,
|
onList: Boolean = true,
|
||||||
@@ -239,10 +246,11 @@ class AnilistMangaViewModel : ViewModel() {
|
|||||||
mangaPopular.postValue(
|
mangaPopular.postValue(
|
||||||
Anilist.query.search(
|
Anilist.query.search(
|
||||||
type,
|
type,
|
||||||
search = search_val,
|
search = searchVal,
|
||||||
onList = if (onList) null else false,
|
onList = if (onList) null else false,
|
||||||
sort = sort,
|
sort = sort,
|
||||||
genres = genres
|
genres = genres,
|
||||||
|
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -257,17 +265,55 @@ class AnilistMangaViewModel : ViewModel() {
|
|||||||
r.sort,
|
r.sort,
|
||||||
r.genres,
|
r.genres,
|
||||||
r.tags,
|
r.tags,
|
||||||
|
r.status,
|
||||||
|
r.source,
|
||||||
r.format,
|
r.format,
|
||||||
|
r.countryOfOrigin,
|
||||||
r.isAdult,
|
r.isAdult,
|
||||||
r.onList,
|
r.onList,
|
||||||
r.excludedGenres,
|
r.excludedGenres,
|
||||||
r.excludedTags,
|
r.excludedTags,
|
||||||
|
r.startYear,
|
||||||
r.seasonYear,
|
r.seasonYear,
|
||||||
r.season
|
r.season,
|
||||||
|
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
var loaded: Boolean = false
|
var loaded: Boolean = false
|
||||||
|
|
||||||
|
private val popularManga: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getPopularManga(): LiveData<MutableList<Media>> = popularManga
|
||||||
|
|
||||||
|
private val popularManhwa: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getPopularManhwa(): LiveData<MutableList<Media>> = popularManhwa
|
||||||
|
|
||||||
|
private val popularNovel: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getPopularNovel(): LiveData<MutableList<Media>> = popularNovel
|
||||||
|
|
||||||
|
private val topRatedManga: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getTopRated(): LiveData<MutableList<Media>> = topRatedManga
|
||||||
|
|
||||||
|
private val mostFavManga: MutableLiveData<MutableList<Media>> =
|
||||||
|
MutableLiveData<MutableList<Media>>(null)
|
||||||
|
|
||||||
|
fun getMostFav(): LiveData<MutableList<Media>> = mostFavManga
|
||||||
|
suspend fun loadAll() {
|
||||||
|
val list = Anilist.query.loadMangaList()
|
||||||
|
popularManga.postValue(list["trendingManga"])
|
||||||
|
popularManhwa.postValue(list["trendingManhwa"])
|
||||||
|
popularNovel.postValue(list["trendingNovel"])
|
||||||
|
topRatedManga.postValue(list["topRated"])
|
||||||
|
mostFavManga.postValue(list["mostFav"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnilistSearch : ViewModel() {
|
class AnilistSearch : ViewModel() {
|
||||||
@@ -286,13 +332,17 @@ class AnilistSearch : ViewModel() {
|
|||||||
r.sort,
|
r.sort,
|
||||||
r.genres,
|
r.genres,
|
||||||
r.tags,
|
r.tags,
|
||||||
|
r.status,
|
||||||
|
r.source,
|
||||||
r.format,
|
r.format,
|
||||||
|
r.countryOfOrigin,
|
||||||
r.isAdult,
|
r.isAdult,
|
||||||
r.onList,
|
r.onList,
|
||||||
r.excludedGenres,
|
r.excludedGenres,
|
||||||
r.excludedTags,
|
r.excludedTags,
|
||||||
|
r.startYear,
|
||||||
r.seasonYear,
|
r.seasonYear,
|
||||||
r.season
|
r.season,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -305,11 +355,15 @@ class AnilistSearch : ViewModel() {
|
|||||||
r.sort,
|
r.sort,
|
||||||
r.genres,
|
r.genres,
|
||||||
r.tags,
|
r.tags,
|
||||||
|
r.status,
|
||||||
|
r.source,
|
||||||
r.format,
|
r.format,
|
||||||
|
r.countryOfOrigin,
|
||||||
r.isAdult,
|
r.isAdult,
|
||||||
r.onList,
|
r.onList,
|
||||||
r.excludedGenres,
|
r.excludedGenres,
|
||||||
r.excludedTags,
|
r.excludedTags,
|
||||||
|
r.startYear,
|
||||||
r.seasonYear,
|
r.seasonYear,
|
||||||
r.season
|
r.season
|
||||||
)
|
)
|
||||||
@@ -347,11 +401,6 @@ class ProfileViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||||
|
|
||||||
private val listImages: MutableLiveData<ArrayList<String?>> =
|
|
||||||
MutableLiveData<ArrayList<String?>>(arrayListOf())
|
|
||||||
|
|
||||||
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
|
||||||
|
|
||||||
suspend fun setData(id: Int) {
|
suspend fun setData(id: Int) {
|
||||||
val res = Anilist.query.initProfilePage(id)
|
val res = Anilist.query.initProfilePage(id)
|
||||||
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
|
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
|
||||||
@@ -367,30 +416,11 @@ class ProfileViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
|
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
|
||||||
|
|
||||||
val bannerImages = arrayListOf<String?>(null, null)
|
|
||||||
val animeRandom = res?.data?.animeMediaList?.lists?.mapNotNull {
|
|
||||||
it.entries?.mapNotNull { entry ->
|
|
||||||
val imageUrl = entry.media?.bannerImage
|
|
||||||
if (imageUrl != null && imageUrl != "null") imageUrl
|
|
||||||
else null
|
|
||||||
}
|
|
||||||
}?.flatten()?.randomOrNull()
|
|
||||||
bannerImages[0] = animeRandom
|
|
||||||
val mangaRandom = res?.data?.mangaMediaList?.lists?.mapNotNull {
|
|
||||||
it.entries?.mapNotNull { entry ->
|
|
||||||
val imageUrl = entry.media?.bannerImage
|
|
||||||
if (imageUrl != null && imageUrl != "null") imageUrl
|
|
||||||
else null
|
|
||||||
}
|
|
||||||
}?.flatten()?.randomOrNull()
|
|
||||||
bannerImages[1] = mangaRandom
|
|
||||||
listImages.postValue(bannerImages)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
mangaFav.postValue(mangaFav.value)
|
mangaFav.postValue(mangaFav.value)
|
||||||
animeFav.postValue(animeFav.value)
|
animeFav.postValue(animeFav.value)
|
||||||
listImages.postValue(listImages.value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
|
|||||||
@@ -11,13 +11,17 @@ data class SearchResults(
|
|||||||
var onList: Boolean? = null,
|
var onList: Boolean? = null,
|
||||||
var perPage: Int? = null,
|
var perPage: Int? = null,
|
||||||
var search: String? = null,
|
var search: String? = null,
|
||||||
|
var countryOfOrigin: String? = null,
|
||||||
var sort: String? = null,
|
var sort: String? = null,
|
||||||
var genres: MutableList<String>? = null,
|
var genres: MutableList<String>? = null,
|
||||||
var excludedGenres: MutableList<String>? = null,
|
var excludedGenres: MutableList<String>? = null,
|
||||||
var tags: MutableList<String>? = null,
|
var tags: MutableList<String>? = null,
|
||||||
var excludedTags: MutableList<String>? = null,
|
var excludedTags: MutableList<String>? = null,
|
||||||
|
var status: String? = null,
|
||||||
|
var source: String? = null,
|
||||||
var format: String? = null,
|
var format: String? = null,
|
||||||
var seasonYear: Int? = null,
|
var seasonYear: Int? = null,
|
||||||
|
var startYear: Int? = null,
|
||||||
var season: String? = null,
|
var season: String? = null,
|
||||||
var page: Int = 1,
|
var page: Int = 1,
|
||||||
var results: MutableList<Media>,
|
var results: MutableList<Media>,
|
||||||
@@ -37,12 +41,24 @@ data class SearchResults(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
status?.let {
|
||||||
|
list.add(SearchChip("STATUS", currContext()!!.getString(R.string.filter_status, it)))
|
||||||
|
}
|
||||||
|
source?.let {
|
||||||
|
list.add(SearchChip("SOURCE", currContext()!!.getString(R.string.filter_source, it)))
|
||||||
|
}
|
||||||
format?.let {
|
format?.let {
|
||||||
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
||||||
}
|
}
|
||||||
|
countryOfOrigin?.let {
|
||||||
|
list.add(SearchChip("COUNTRY", currContext()!!.getString(R.string.filter_country, it)))
|
||||||
|
}
|
||||||
season?.let {
|
season?.let {
|
||||||
list.add(SearchChip("SEASON", it))
|
list.add(SearchChip("SEASON", it))
|
||||||
}
|
}
|
||||||
|
startYear?.let {
|
||||||
|
list.add(SearchChip("START_YEAR", it.toString()))
|
||||||
|
}
|
||||||
seasonYear?.let {
|
seasonYear?.let {
|
||||||
list.add(SearchChip("SEASON_YEAR", it.toString()))
|
list.add(SearchChip("SEASON_YEAR", it.toString()))
|
||||||
}
|
}
|
||||||
@@ -74,8 +90,12 @@ data class SearchResults(
|
|||||||
fun removeChip(chip: SearchChip) {
|
fun removeChip(chip: SearchChip) {
|
||||||
when (chip.type) {
|
when (chip.type) {
|
||||||
"SORT" -> sort = null
|
"SORT" -> sort = null
|
||||||
|
"STATUS" -> status = null
|
||||||
|
"SOURCE" -> source = null
|
||||||
"FORMAT" -> format = null
|
"FORMAT" -> format = null
|
||||||
|
"COUNTRY" -> countryOfOrigin = null
|
||||||
"SEASON" -> season = null
|
"SEASON" -> season = null
|
||||||
|
"START_YEAR" -> startYear = null
|
||||||
"SEASON_YEAR" -> seasonYear = null
|
"SEASON_YEAR" -> seasonYear = null
|
||||||
"GENRE" -> genres?.remove(chip.text)
|
"GENRE" -> genres?.remove(chip.text)
|
||||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ data class CharacterConnection(
|
|||||||
@SerialName("nodes") var nodes: List<Character>?,
|
@SerialName("nodes") var nodes: List<Character>?,
|
||||||
|
|
||||||
// The pagination information
|
// The pagination information
|
||||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
@SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||||
) : java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -72,7 +72,7 @@ data class CharacterEdge(
|
|||||||
@SerialName("name") var name: String?,
|
@SerialName("name") var name: String?,
|
||||||
|
|
||||||
// The voice actors of the character
|
// The voice actors of the character
|
||||||
// @SerialName("voiceActors") var voiceActors: List<Staff>?,
|
@SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||||
|
|
||||||
// The voice actors of the character with role date
|
// The voice actors of the character with role date
|
||||||
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,
|
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ class Query {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("Media")
|
@SerialName("Media")
|
||||||
val media: ani.dantotsu.connections.anilist.api.Media?
|
val media: ani.dantotsu.connections.anilist.api.Media?,
|
||||||
|
@SerialName("Page")
|
||||||
|
val page: ani.dantotsu.connections.anilist.api.Page?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,9 +149,45 @@ class Query {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
|
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
|
||||||
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
|
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?
|
||||||
@SerialName("animeMediaList") val animeMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
)
|
||||||
@SerialName("mangaMediaList") val mangaMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AnimeList(
|
||||||
|
@SerialName("data")
|
||||||
|
val data: Data?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Data(
|
||||||
|
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("recentUpdates2") val recentUpdates2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("trendingMovies2") val trendingMovies2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MangaList(
|
||||||
|
@SerialName("data")
|
||||||
|
val data: Data?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Data(
|
||||||
|
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
|
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,13 +321,13 @@ class Query {
|
|||||||
val statistics: NNUserStatisticTypes,
|
val statistics: NNUserStatisticTypes,
|
||||||
@SerialName("siteUrl")
|
@SerialName("siteUrl")
|
||||||
val siteUrl: String,
|
val siteUrl: String,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NNUserStatisticTypes(
|
data class NNUserStatisticTypes(
|
||||||
@SerialName("anime") var anime: NNUserStatistics,
|
@SerialName("anime") var anime: NNUserStatistics,
|
||||||
@SerialName("manga") var manga: NNUserStatistics
|
@SerialName("manga") var manga: NNUserStatistics
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NNUserStatistics(
|
data class NNUserStatistics(
|
||||||
@@ -300,9 +338,9 @@ class Query {
|
|||||||
@SerialName("episodesWatched") var episodesWatched: Int,
|
@SerialName("episodesWatched") var episodesWatched: Int,
|
||||||
@SerialName("chaptersRead") var chaptersRead: Int,
|
@SerialName("chaptersRead") var chaptersRead: Int,
|
||||||
@SerialName("volumesRead") var volumesRead: Int,
|
@SerialName("volumesRead") var volumesRead: Int,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserFavourites(
|
data class UserFavourites(
|
||||||
@SerialName("anime")
|
@SerialName("anime")
|
||||||
val anime: UserMediaFavouritesCollection,
|
val anime: UserMediaFavouritesCollection,
|
||||||
@@ -314,13 +352,13 @@ class Query {
|
|||||||
val staff: UserStaffFavouritesCollection,
|
val staff: UserStaffFavouritesCollection,
|
||||||
@SerialName("studios")
|
@SerialName("studios")
|
||||||
val studios: UserStudioFavouritesCollection,
|
val studios: UserStudioFavouritesCollection,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserMediaFavouritesCollection(
|
data class UserMediaFavouritesCollection(
|
||||||
@SerialName("nodes")
|
@SerialName("nodes")
|
||||||
val nodes: List<UserMediaImageFavorite>,
|
val nodes: List<UserMediaImageFavorite>,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserMediaImageFavorite(
|
data class UserMediaImageFavorite(
|
||||||
@@ -328,13 +366,13 @@ class Query {
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
@SerialName("coverImage")
|
@SerialName("coverImage")
|
||||||
val coverImage: MediaCoverImage
|
val coverImage: MediaCoverImage
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserCharacterFavouritesCollection(
|
data class UserCharacterFavouritesCollection(
|
||||||
@SerialName("nodes")
|
@SerialName("nodes")
|
||||||
val nodes: List<UserCharacterImageFavorite>,
|
val nodes: List<UserCharacterImageFavorite>,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserCharacterImageFavorite(
|
data class UserCharacterImageFavorite(
|
||||||
@@ -346,19 +384,19 @@ class Query {
|
|||||||
val image: CharacterImage,
|
val image: CharacterImage,
|
||||||
@SerialName("isFavourite")
|
@SerialName("isFavourite")
|
||||||
val isFavourite: Boolean
|
val isFavourite: Boolean
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserStaffFavouritesCollection(
|
data class UserStaffFavouritesCollection(
|
||||||
@SerialName("nodes")
|
@SerialName("nodes")
|
||||||
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
|
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserStudioFavouritesCollection(
|
data class UserStudioFavouritesCollection(
|
||||||
@SerialName("nodes")
|
@SerialName("nodes")
|
||||||
val nodes: List<UserStudioFavorite>,
|
val nodes: List<UserStudioFavorite>,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserStudioFavorite(
|
data class UserStudioFavorite(
|
||||||
@@ -366,7 +404,7 @@ class Query {
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
@SerialName("name")
|
@SerialName("name")
|
||||||
val name: String,
|
val name: String,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
// Statistics
|
// Statistics
|
||||||
@@ -375,12 +413,12 @@ class Query {
|
|||||||
data class StatisticsResponse(
|
data class StatisticsResponse(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
): java.io.Serializable {
|
) : java.io.Serializable {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("User")
|
@SerialName("User")
|
||||||
val user: StatisticsUser?
|
val user: StatisticsUser?
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ enum class NotificationType(val value: String) {
|
|||||||
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
|
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
|
||||||
MEDIA_MERGE("MEDIA_MERGE"),
|
MEDIA_MERGE("MEDIA_MERGE"),
|
||||||
MEDIA_DELETION("MEDIA_DELETION"),
|
MEDIA_DELETION("MEDIA_DELETION"),
|
||||||
|
|
||||||
//custom
|
//custom
|
||||||
COMMENT_REPLY("COMMENT_REPLY"),
|
COMMENT_REPLY("COMMENT_REPLY"),
|
||||||
}
|
}
|
||||||
@@ -84,9 +85,9 @@ data class Notification(
|
|||||||
@SerialName("createdAt")
|
@SerialName("createdAt")
|
||||||
val createdAt: Int,
|
val createdAt: Int,
|
||||||
@SerialName("media")
|
@SerialName("media")
|
||||||
val media: ani.dantotsu.connections.anilist.api.Media? = null,
|
val media: Media? = null,
|
||||||
@SerialName("user")
|
@SerialName("user")
|
||||||
val user: ani.dantotsu.connections.anilist.api.User? = null,
|
val user: User? = null,
|
||||||
@SerialName("message")
|
@SerialName("message")
|
||||||
val message: MessageActivity? = null,
|
val message: MessageActivity? = null,
|
||||||
@SerialName("activity")
|
@SerialName("activity")
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ data class StaffConnection(
|
|||||||
// The pagination information
|
// The pagination information
|
||||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StaffImage(
|
data class StaffImage(
|
||||||
// The character's image of media at its largest size
|
// The character's image of media at its largest size
|
||||||
@@ -101,6 +102,7 @@ data class StaffImage(
|
|||||||
// The character's image of media at medium size
|
// The character's image of media at medium size
|
||||||
@SerialName("medium") var medium: String?,
|
@SerialName("medium") var medium: String?,
|
||||||
) : java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StaffEdge(
|
data class StaffEdge(
|
||||||
var role: String?,
|
var role: String?,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ data class UserAvatar(
|
|||||||
|
|
||||||
// The avatar of user at medium size
|
// The avatar of user at medium size
|
||||||
@SerialName("medium") var medium: String?,
|
@SerialName("medium") var medium: String?,
|
||||||
): java.io.Serializable
|
) : java.io.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserStatisticTypes(
|
data class UserStatisticTypes(
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package ani.dantotsu.connections.bakaupdates
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
|
import ani.dantotsu.tryWithSuspend
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okio.ByteString.Companion.encode
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
|
||||||
|
class MangaUpdates {
|
||||||
|
|
||||||
|
private val Int?.dateFormat get() = String.format("%02d", this)
|
||||||
|
|
||||||
|
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
|
||||||
|
|
||||||
|
suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
|
||||||
|
return tryWithSuspend {
|
||||||
|
val query = JSONObject().apply {
|
||||||
|
try {
|
||||||
|
put("search", title.encode(Charset.forName("UTF-8")))
|
||||||
|
startDate?.let {
|
||||||
|
put(
|
||||||
|
"start_date",
|
||||||
|
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
put("include_metadata", true)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val res = client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
|
||||||
|
coroutineScope {
|
||||||
|
res.results?.map {
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
Logger.log(it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}?.awaitAll()
|
||||||
|
res.results?.first {
|
||||||
|
it.metadata.series.lastUpdated?.timestamp != null
|
||||||
|
&& (it.metadata.series.latestChapter != null
|
||||||
|
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
|
||||||
|
return results.metadata.series.latestChapter?.let {
|
||||||
|
context.getString(R.string.chapter_number, it)
|
||||||
|
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
|
||||||
|
chapter.takeIf {
|
||||||
|
it.toIntOrNull() == null
|
||||||
|
} ?: context.getString(R.string.chapter_number, chapter.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MangaUpdatesResponse(
|
||||||
|
@SerialName("total_hits")
|
||||||
|
val totalHits: Int?,
|
||||||
|
@SerialName("page")
|
||||||
|
val page: Int?,
|
||||||
|
@SerialName("per_page")
|
||||||
|
val perPage: Int?,
|
||||||
|
val results: List<Results>? = null
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Results(
|
||||||
|
val record: Record,
|
||||||
|
val metadata: MetaData
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Record(
|
||||||
|
@SerialName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("title")
|
||||||
|
val title: String,
|
||||||
|
@SerialName("volume")
|
||||||
|
val volume: String?,
|
||||||
|
@SerialName("chapter")
|
||||||
|
val chapter: String?,
|
||||||
|
@SerialName("release_date")
|
||||||
|
val releaseDate: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MetaData(
|
||||||
|
val series: Series
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Series(
|
||||||
|
@SerialName("series_id")
|
||||||
|
val seriesId: Long?,
|
||||||
|
@SerialName("title")
|
||||||
|
val title: String?,
|
||||||
|
@SerialName("latest_chapter")
|
||||||
|
val latestChapter: Int?,
|
||||||
|
@SerialName("last_updated")
|
||||||
|
val lastUpdated: LastUpdated?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class LastUpdated(
|
||||||
|
@SerialName("timestamp")
|
||||||
|
val timestamp: Long,
|
||||||
|
@SerialName("as_rfc3339")
|
||||||
|
val asRfc3339: String,
|
||||||
|
@SerialName("as_string")
|
||||||
|
val asString: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
object CommentsAPI {
|
object CommentsAPI {
|
||||||
val address: String = "https://1224665.xyz:443"
|
private const val ADDRESS: String = "https://1224665.xyz:443"
|
||||||
var authToken: String? = null
|
var authToken: String? = null
|
||||||
var userId: String? = null
|
var userId: String? = null
|
||||||
var isBanned: Boolean = false
|
var isBanned: Boolean = false
|
||||||
@@ -32,8 +32,13 @@ object CommentsAPI {
|
|||||||
var isMod: Boolean = false
|
var isMod: Boolean = false
|
||||||
var totalVotes: Int = 0
|
var totalVotes: Int = 0
|
||||||
|
|
||||||
suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? {
|
suspend fun getCommentsForId(
|
||||||
var url = "$address/comments/$id/$page"
|
id: Int,
|
||||||
|
page: Int = 1,
|
||||||
|
tag: Int?,
|
||||||
|
sort: String?
|
||||||
|
): CommentResponse? {
|
||||||
|
var url = "$ADDRESS/comments/$id/$page"
|
||||||
val request = requestBuilder()
|
val request = requestBuilder()
|
||||||
tag?.let {
|
tag?.let {
|
||||||
url += "?tag=$it"
|
url += "?tag=$it"
|
||||||
@@ -61,7 +66,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
|
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
|
||||||
val url = "$address/comments/parent/$id/$page"
|
val url = "$ADDRESS/comments/parent/$id/$page"
|
||||||
val request = requestBuilder()
|
val request = requestBuilder()
|
||||||
val json = try {
|
val json = try {
|
||||||
request.get(url)
|
request.get(url)
|
||||||
@@ -83,7 +88,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSingleComment(id: Int): Comment? {
|
suspend fun getSingleComment(id: Int): Comment? {
|
||||||
val url = "$address/comments/$id"
|
val url = "$ADDRESS/comments/$id"
|
||||||
val request = requestBuilder()
|
val request = requestBuilder()
|
||||||
val json = try {
|
val json = try {
|
||||||
request.get(url)
|
request.get(url)
|
||||||
@@ -105,7 +110,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun vote(commentId: Int, voteType: Int): Boolean {
|
suspend fun vote(commentId: Int, voteType: Int): Boolean {
|
||||||
val url = "$address/comments/vote/$commentId/$voteType"
|
val url = "$ADDRESS/comments/vote/$commentId/$voteType"
|
||||||
val request = requestBuilder()
|
val request = requestBuilder()
|
||||||
val json = try {
|
val json = try {
|
||||||
request.post(url)
|
request.post(url)
|
||||||
@@ -121,7 +126,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
|
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
|
||||||
val url = "$address/comments"
|
val url = "$ADDRESS/comments"
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
.add("user_id", userId ?: return null)
|
.add("user_id", userId ?: return null)
|
||||||
.add("media_id", mediaId.toString())
|
.add("media_id", mediaId.toString())
|
||||||
@@ -169,7 +174,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteComment(commentId: Int): Boolean {
|
suspend fun deleteComment(commentId: Int): Boolean {
|
||||||
val url = "$address/comments/$commentId"
|
val url = "$ADDRESS/comments/$commentId"
|
||||||
val request = requestBuilder()
|
val request = requestBuilder()
|
||||||
val json = try {
|
val json = try {
|
||||||
request.delete(url)
|
request.delete(url)
|
||||||
@@ -185,7 +190,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun editComment(commentId: Int, content: String): Boolean {
|
suspend fun editComment(commentId: Int, content: String): Boolean {
|
||||||
val url = "$address/comments/$commentId"
|
val url = "$ADDRESS/comments/$commentId"
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
.add("content", content)
|
.add("content", content)
|
||||||
.build()
|
.build()
|
||||||
@@ -204,7 +209,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun banUser(userId: String): Boolean {
|
suspend fun banUser(userId: String): Boolean {
|
||||||
val url = "$address/ban/$userId"
|
val url = "$ADDRESS/ban/$userId"
|
||||||
val request = requestBuilder()
|
val request = requestBuilder()
|
||||||
val json = try {
|
val json = try {
|
||||||
request.post(url)
|
request.post(url)
|
||||||
@@ -225,7 +230,7 @@ object CommentsAPI {
|
|||||||
mediaTitle: String,
|
mediaTitle: String,
|
||||||
reportedId: String
|
reportedId: String
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val url = "$address/report/$commentId"
|
val url = "$ADDRESS/report/$commentId"
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
.add("username", username)
|
.add("username", username)
|
||||||
.add("mediaName", mediaTitle)
|
.add("mediaName", mediaTitle)
|
||||||
@@ -247,7 +252,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
|
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
|
||||||
val url = "$address/notification/reply"
|
val url = "$ADDRESS/notification/reply"
|
||||||
val request = requestBuilder(client)
|
val request = requestBuilder(client)
|
||||||
val json = try {
|
val json = try {
|
||||||
request.get(url)
|
request.get(url)
|
||||||
@@ -268,7 +273,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
|
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
|
||||||
val url = "$address/user"
|
val url = "$ADDRESS/user"
|
||||||
val request = if (client != null) requestBuilder(client) else requestBuilder()
|
val request = if (client != null) requestBuilder(client) else requestBuilder()
|
||||||
val json = try {
|
val json = try {
|
||||||
request.get(url)
|
request.get(url)
|
||||||
@@ -310,7 +315,7 @@ object CommentsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
val url = "$address/authenticate"
|
val url = "$ADDRESS/authenticate"
|
||||||
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
|
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
|
||||||
repeat(MAX_RETRIES) {
|
repeat(MAX_RETRIES) {
|
||||||
try {
|
try {
|
||||||
@@ -348,6 +353,17 @@ object CommentsAPI {
|
|||||||
snackString("Failed to login after multiple attempts")
|
snackString("Failed to login after multiple attempts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
PrefManager.removeVal(PrefName.CommentAuthResponse)
|
||||||
|
PrefManager.removeVal(PrefName.CommentTokenExpiry)
|
||||||
|
authToken = null
|
||||||
|
userId = null
|
||||||
|
isBanned = false
|
||||||
|
isAdmin = false
|
||||||
|
isMod = false
|
||||||
|
totalVotes = 0
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun authRequest(
|
private suspend fun authRequest(
|
||||||
token: String,
|
token: String,
|
||||||
url: String,
|
url: String,
|
||||||
@@ -388,7 +404,7 @@ object CommentsAPI {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
val message = parsed?.message ?: reason ?: error
|
val message = parsed?.message ?: reason ?: error
|
||||||
val fullMessage = if(code == 500) message else "$code: $message"
|
val fullMessage = if (code == 500) message else "$code: $message"
|
||||||
|
|
||||||
toast(fullMessage)
|
toast(fullMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class CrashlyticsStub : CrashlyticsInterface {
|
|||||||
override fun initialize(context: Context) {
|
override fun initialize(context: Context) {
|
||||||
//no-op
|
//no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logException(e: Throwable) {
|
override fun logException(e: Throwable) {
|
||||||
Logger.log(e)
|
Logger.log(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ object Discord {
|
|||||||
var avatar: String? = null
|
var avatar: String? = null
|
||||||
|
|
||||||
|
|
||||||
fun getSavedToken(context: Context): Boolean {
|
fun getSavedToken(): Boolean {
|
||||||
token = PrefManager.getVal(
|
token = PrefManager.getVal(
|
||||||
PrefName.DiscordToken, null as String?
|
PrefName.DiscordToken, null as String?
|
||||||
)
|
)
|
||||||
return token != null
|
return token != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToken(context: Context, token: String) {
|
fun saveToken(token: String) {
|
||||||
PrefManager.setVal(PrefName.DiscordToken, token)
|
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,4 +71,6 @@ object Discord {
|
|||||||
const val application_Id = "1163925779692912771"
|
const val application_Id = "1163925779692912771"
|
||||||
const val small_Image: String =
|
const val small_Image: String =
|
||||||
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
|
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
|
||||||
|
const val small_Image_AniList: String =
|
||||||
|
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
|
||||||
}
|
}
|
||||||
@@ -5,16 +5,12 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.MediaStore
|
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
@@ -37,7 +33,6 @@ import okhttp3.Response
|
|||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
|
|
||||||
class DiscordService : Service() {
|
class DiscordService : Service() {
|
||||||
private var heartbeat: Int = 0
|
private var heartbeat: Int = 0
|
||||||
@@ -49,6 +44,7 @@ class DiscordService : Service() {
|
|||||||
private lateinit var heartbeatThread: Thread
|
private lateinit var heartbeatThread: Thread
|
||||||
private lateinit var client: OkHttpClient
|
private lateinit var client: OkHttpClient
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
private val shouldLog = false
|
||||||
var presenceStore = ""
|
var presenceStore = ""
|
||||||
val json = Json {
|
val json = Json {
|
||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
@@ -67,7 +63,7 @@ class DiscordService : Service() {
|
|||||||
PowerManager.PARTIAL_WAKE_LOCK,
|
PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
"discordRPC:backgroundPresence"
|
"discordRPC:backgroundPresence"
|
||||||
)
|
)
|
||||||
wakeLock.acquire()
|
wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
|
||||||
log("WakeLock Acquired")
|
log("WakeLock Acquired")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val serviceChannel = NotificationChannel(
|
val serviceChannel = NotificationChannel(
|
||||||
@@ -162,8 +158,8 @@ class DiscordService : Service() {
|
|||||||
|
|
||||||
inner class DiscordWebSocketListener : WebSocketListener() {
|
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||||
|
|
||||||
var retryAttempts = 0
|
private var retryAttempts = 0
|
||||||
val maxRetryAttempts = 10
|
private val maxRetryAttempts = 10
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
super.onOpen(webSocket, response)
|
super.onOpen(webSocket, response)
|
||||||
this@DiscordService.webSocket = webSocket
|
this@DiscordService.webSocket = webSocket
|
||||||
@@ -232,7 +228,7 @@ class DiscordService : Service() {
|
|||||||
resume()
|
resume()
|
||||||
resume = false
|
resume = false
|
||||||
} else {
|
} else {
|
||||||
identify(webSocket, baseContext)
|
identify(webSocket)
|
||||||
log("WebSocket: Identified")
|
log("WebSocket: Identified")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,13 +241,13 @@ class DiscordService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun identify(webSocket: WebSocket, context: Context) {
|
private fun identify(webSocket: WebSocket) {
|
||||||
val properties = JsonObject()
|
val properties = JsonObject()
|
||||||
properties.addProperty("os", "linux")
|
properties.addProperty("os", "linux")
|
||||||
properties.addProperty("browser", "unknown")
|
properties.addProperty("browser", "unknown")
|
||||||
properties.addProperty("device", "unknown")
|
properties.addProperty("device", "unknown")
|
||||||
val d = JsonObject()
|
val d = JsonObject()
|
||||||
d.addProperty("token", getToken(context))
|
d.addProperty("token", getToken())
|
||||||
d.addProperty("intents", 0)
|
d.addProperty("intents", 0)
|
||||||
d.add("properties", properties)
|
d.add("properties", properties)
|
||||||
val payload = JsonObject()
|
val payload = JsonObject()
|
||||||
@@ -270,7 +266,7 @@ class DiscordService : Service() {
|
|||||||
retryAttempts++
|
retryAttempts++
|
||||||
if (retryAttempts >= maxRetryAttempts) {
|
if (retryAttempts >= maxRetryAttempts) {
|
||||||
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
|
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
|
||||||
errorNotification("Could not set the presence", "Max Retry Attempts")
|
errorNotification("Timeout setting presence", "Max Retry Attempts")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +307,7 @@ class DiscordService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToken(context: Context): String {
|
fun getToken(): String {
|
||||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||||
return if (token == null) {
|
return if (token == null) {
|
||||||
log("WebSocket: Token not found")
|
log("WebSocket: Token not found")
|
||||||
@@ -349,13 +345,13 @@ class DiscordService : Service() {
|
|||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
//TODO: Request permission
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notificationManager.notify(2, builder.build())
|
notificationManager.notify(2, builder.build())
|
||||||
log("Error Notified")
|
log("Error Notified")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
fun saveSimpleTestPresence() {
|
fun saveSimpleTestPresence() {
|
||||||
val file = File(baseContext.cacheDir, "payload")
|
val file = File(baseContext.cacheDir, "payload")
|
||||||
//fill with test payload
|
//fill with test payload
|
||||||
@@ -375,20 +371,22 @@ class DiscordService : Service() {
|
|||||||
log("WebSocket: Simple Test Presence Saved")
|
log("WebSocket: Simple Test Presence Saved")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPresence(String: String) {
|
fun setPresence(string: String) {
|
||||||
log("WebSocket: Sending Presence payload")
|
log("WebSocket: Sending Presence payload")
|
||||||
log(String)
|
log(string)
|
||||||
webSocket.send(String)
|
webSocket.send(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun log(string: String) {
|
fun log(string: String) {
|
||||||
//Logger.log(string)
|
if (shouldLog) {
|
||||||
|
Logger.log(string)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resume() {
|
fun resume() {
|
||||||
log("Sending Resume payload")
|
log("Sending Resume payload")
|
||||||
val d = JsonObject()
|
val d = JsonObject()
|
||||||
d.addProperty("token", getToken(baseContext))
|
d.addProperty("token", getToken())
|
||||||
d.addProperty("session_id", sessionId)
|
d.addProperty("session_id", sessionId)
|
||||||
d.addProperty("seq", sequence)
|
d.addProperty("seq", sequence)
|
||||||
val json = JsonObject()
|
val json = JsonObject()
|
||||||
@@ -404,7 +402,7 @@ class DiscordService : Service() {
|
|||||||
Thread.sleep(heartbeat.toLong())
|
Thread.sleep(heartbeat.toLong())
|
||||||
heartbeatSend(webSocket, sequence)
|
heartbeatSend(webSocket, sequence)
|
||||||
log("WebSocket: Heartbeat Sent")
|
log("WebSocket: Heartbeat Sent")
|
||||||
} catch (e: InterruptedException) {
|
} catch (ignored: InterruptedException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class Login : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
||||||
finish()
|
finish()
|
||||||
saveToken(this, token)
|
saveToken(token)
|
||||||
startMainActivity(this@Login)
|
startMainActivity(this@Login)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
|||||||
assets = Activity.Assets(
|
assets = Activity.Assets(
|
||||||
largeImage = data.largeImage?.url?.discordUrl(),
|
largeImage = data.largeImage?.url?.discordUrl(),
|
||||||
largeText = data.largeImage?.label,
|
largeText = data.largeImage?.label,
|
||||||
smallImage = data.smallImage?.url?.discordUrl(),
|
smallImage = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(),
|
||||||
smallText = data.smallImage?.label
|
smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu",
|
||||||
),
|
),
|
||||||
buttons = data.buttons.map { it.label },
|
buttons = data.buttons.map { it.label },
|
||||||
metadata = Activity.Metadata(
|
metadata = Activity.Metadata(
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package ani.dantotsu.connections.github
|
||||||
|
|
||||||
|
import ani.dantotsu.Mapper
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.getAppString
|
||||||
|
import ani.dantotsu.settings.Developer
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
class Contributors {
|
||||||
|
|
||||||
|
fun getContributors(): Array<Developer> {
|
||||||
|
var developers = arrayOf<Developer>()
|
||||||
|
runBlocking(Dispatchers.IO) {
|
||||||
|
val repo = getAppString(R.string.repo)
|
||||||
|
val res = client.get("https://api.github.com/repos/$repo/contributors")
|
||||||
|
.parsed<JsonArray>().map {
|
||||||
|
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||||
|
}
|
||||||
|
res.forEach {
|
||||||
|
if (it.login == "SunglassJerry") return@forEach
|
||||||
|
val role = when (it.login) {
|
||||||
|
"rebelonion" -> "Owner & Maintainer"
|
||||||
|
"sneazy-ibo" -> "Contributor & Comment Moderator"
|
||||||
|
"WaiWhat" -> "Icon Designer"
|
||||||
|
else -> "Contributor"
|
||||||
|
}
|
||||||
|
developers = developers.plus(
|
||||||
|
Developer(
|
||||||
|
it.login,
|
||||||
|
it.avatarUrl,
|
||||||
|
role,
|
||||||
|
it.htmlUrl
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
developers = developers.plus(
|
||||||
|
arrayOf(
|
||||||
|
Developer(
|
||||||
|
"MarshMeadow",
|
||||||
|
"https://avatars.githubusercontent.com/u/88599122?v=4",
|
||||||
|
"Beta Icon Designer & Website Maintainer",
|
||||||
|
"https://github.com/MarshMeadow?tab=repositories"
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
"Zaxx69",
|
||||||
|
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6342562-kxE8m4i7KUMK.png",
|
||||||
|
"Telegram Admin",
|
||||||
|
"https://anilist.co/user/6342562"
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
"Arif Alam",
|
||||||
|
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6011177-2n994qtayiR9.jpg",
|
||||||
|
"Discord & Comment Moderator",
|
||||||
|
"https://anilist.co/user/6011177"
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
"SunglassJeery",
|
||||||
|
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b5804776-FEKfP5wbz2xv.png",
|
||||||
|
"Head Discord & Comment Moderator",
|
||||||
|
"https://anilist.co/user/5804776"
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
"Excited",
|
||||||
|
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6131921-toSoGWmKbRA1.png",
|
||||||
|
"Comment Moderator",
|
||||||
|
"https://anilist.co/user/6131921"
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
"Gurjshan",
|
||||||
|
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6363228-rWQ3Pl3WuxzL.png",
|
||||||
|
"Comment Moderator",
|
||||||
|
"https://anilist.co/user/6363228"
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
"NekoMimi",
|
||||||
|
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6244220-HOpImMGMQAxW.jpg",
|
||||||
|
"Comment Moderator",
|
||||||
|
"https://anilist.co/user/6244220"
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
"Zaidsenior",
|
||||||
|
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg",
|
||||||
|
"Comment Moderator",
|
||||||
|
"https://anilist.co/user/6049773"
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
"hastsu",
|
||||||
|
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
|
||||||
|
"Comment Moderator",
|
||||||
|
"https://anilist.co/user/6183359"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return developers
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GithubResponse(
|
||||||
|
@SerialName("login")
|
||||||
|
val login: String,
|
||||||
|
@SerialName("avatar_url")
|
||||||
|
val avatarUrl: String,
|
||||||
|
@SerialName("html_url")
|
||||||
|
val htmlUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
54
app/src/main/java/ani/dantotsu/connections/github/Forks.kt
Normal file
54
app/src/main/java/ani/dantotsu/connections/github/Forks.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package ani.dantotsu.connections.github
|
||||||
|
|
||||||
|
import ani.dantotsu.Mapper
|
||||||
|
import ani.dantotsu.client
|
||||||
|
import ani.dantotsu.settings.Developer
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
class Forks {
|
||||||
|
|
||||||
|
fun getForks(): Array<Developer> {
|
||||||
|
var forks = arrayOf<Developer>()
|
||||||
|
runBlocking(Dispatchers.IO) {
|
||||||
|
val res =
|
||||||
|
client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks?sort=stargazers")
|
||||||
|
.parsed<JsonArray>().map {
|
||||||
|
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||||
|
}
|
||||||
|
res.forEach {
|
||||||
|
forks = forks.plus(
|
||||||
|
Developer(
|
||||||
|
it.name,
|
||||||
|
it.owner.avatarUrl,
|
||||||
|
it.owner.login,
|
||||||
|
it.htmlUrl
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return forks
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GithubResponse(
|
||||||
|
@SerialName("name")
|
||||||
|
val name: String,
|
||||||
|
val owner: Owner,
|
||||||
|
@SerialName("html_url")
|
||||||
|
val htmlUrl: String,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Owner(
|
||||||
|
@SerialName("login")
|
||||||
|
val login: String,
|
||||||
|
@SerialName("avatar_url")
|
||||||
|
val avatarUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.client
|
import ani.dantotsu.client
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
@@ -64,7 +63,7 @@ object MAL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
suspend fun getSavedToken(): Boolean {
|
||||||
return tryWithSuspend(false) {
|
return tryWithSuspend(false) {
|
||||||
var res: ResponseToken =
|
var res: ResponseToken =
|
||||||
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||||
@@ -77,7 +76,7 @@ object MAL {
|
|||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSavedToken(context: Context) {
|
fun removeSavedToken() {
|
||||||
token = null
|
token = null
|
||||||
username = null
|
username = null
|
||||||
userid = null
|
userid = null
|
||||||
|
|||||||
381
app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
Normal file
381
app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
package ani.dantotsu.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.widget.Toast
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.download.anime.OfflineAnimeModel
|
||||||
|
import ani.dantotsu.download.manga.OfflineMangaModel
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.parsers.Episode
|
||||||
|
import ani.dantotsu.parsers.MangaChapter
|
||||||
|
import ani.dantotsu.parsers.MangaImage
|
||||||
|
import ani.dantotsu.parsers.Subtitle
|
||||||
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
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 eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
class DownloadCompat {
|
||||||
|
companion object {
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
fun loadMediaCompat(downloadedType: DownloadedType): Media? {
|
||||||
|
val type = when (downloadedType.type) {
|
||||||
|
MediaType.MANGA -> "Manga"
|
||||||
|
MediaType.ANIME -> "Anime"
|
||||||
|
else -> "Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$type/${downloadedType.titleName}"
|
||||||
|
)
|
||||||
|
//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.log("Error loading media.json: ${e.message}")
|
||||||
|
Logger.log(e)
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
fun loadOfflineAnimeModelCompat(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||||
|
val type = when (downloadedType.type) {
|
||||||
|
MediaType.MANGA -> "Manga"
|
||||||
|
MediaType.ANIME -> "Anime"
|
||||||
|
else -> "Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$type/${downloadedType.titleName}"
|
||||||
|
)
|
||||||
|
//load media.json and convert to media class with gson
|
||||||
|
try {
|
||||||
|
val mediaModel = loadMediaCompat(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.log("Error loading media.json: ${e.message}")
|
||||||
|
Logger.log(e)
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
|
return OfflineAnimeModel(
|
||||||
|
"unknown",
|
||||||
|
"0",
|
||||||
|
"??",
|
||||||
|
"??",
|
||||||
|
"??",
|
||||||
|
"movie",
|
||||||
|
"hmm",
|
||||||
|
isOngoing = false,
|
||||||
|
isUserScored = false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
fun loadOfflineMangaModelCompat(downloadedType: DownloadedType): OfflineMangaModel {
|
||||||
|
val type = when (downloadedType.type) {
|
||||||
|
MediaType.MANGA -> "Manga"
|
||||||
|
MediaType.ANIME -> "Anime"
|
||||||
|
else -> "Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$type/${downloadedType.titleName}"
|
||||||
|
)
|
||||||
|
//load media.json and convert to media class with gson
|
||||||
|
try {
|
||||||
|
val mediaModel = loadMediaCompat(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.log("Error loading media.json: ${e.message}")
|
||||||
|
Logger.log(e)
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
|
return OfflineMangaModel(
|
||||||
|
"unknown",
|
||||||
|
"0",
|
||||||
|
"??",
|
||||||
|
"??",
|
||||||
|
"movie",
|
||||||
|
"hmm",
|
||||||
|
isOngoing = false,
|
||||||
|
isUserScored = false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
suspend fun loadEpisodesCompat(
|
||||||
|
animeLink: String,
|
||||||
|
extra: Map<String, String>?,
|
||||||
|
sAnime: SAnime
|
||||||
|
): List<Episode> {
|
||||||
|
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"${animeLocation}/$animeLink"
|
||||||
|
)
|
||||||
|
//get all of the folder names and add them to the list
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
if (directory.exists()) {
|
||||||
|
directory.listFiles()?.forEach {
|
||||||
|
//put the title and episdode number in the extra data
|
||||||
|
val extraData = mutableMapOf<String, String>()
|
||||||
|
extraData["title"] = animeLink
|
||||||
|
extraData["episode"] = it.name
|
||||||
|
if (it.isDirectory) {
|
||||||
|
val episode = Episode(
|
||||||
|
it.name,
|
||||||
|
"$animeLink - ${it.name}",
|
||||||
|
it.name,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
extra = extraData,
|
||||||
|
sEpisode = SEpisodeImpl()
|
||||||
|
)
|
||||||
|
episodes.add(episode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
|
||||||
|
return episodes
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
suspend fun loadChaptersCompat(
|
||||||
|
mangaLink: String,
|
||||||
|
extra: Map<String, String>?,
|
||||||
|
sManga: SManga
|
||||||
|
): List<MangaChapter> {
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/$mangaLink"
|
||||||
|
)
|
||||||
|
//get all of the folder names and add them to the list
|
||||||
|
val chapters = mutableListOf<MangaChapter>()
|
||||||
|
if (directory.exists()) {
|
||||||
|
directory.listFiles()?.forEach {
|
||||||
|
if (it.isDirectory) {
|
||||||
|
val chapter = MangaChapter(
|
||||||
|
it.name,
|
||||||
|
"$mangaLink/${it.name}",
|
||||||
|
it.name,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
SChapter.create()
|
||||||
|
)
|
||||||
|
chapters.add(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
suspend fun loadImagesCompat(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/$chapterLink"
|
||||||
|
)
|
||||||
|
val images = mutableListOf<MangaImage>()
|
||||||
|
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
|
||||||
|
if (directory.exists()) {
|
||||||
|
directory.listFiles()?.forEach {
|
||||||
|
if (it.isFile) {
|
||||||
|
val image = MangaImage(it.absolutePath, false, null)
|
||||||
|
images.add(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
images.sortBy { image ->
|
||||||
|
val matchResult = imageNumberRegex.find(image.url.url)
|
||||||
|
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
for (image in images) {
|
||||||
|
Logger.log("imageNumber: ${image.url.url}")
|
||||||
|
}
|
||||||
|
return images
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
fun loadSubtitleCompat(title: String, episode: String): List<Subtitle>? {
|
||||||
|
currContext()?.let {
|
||||||
|
File(
|
||||||
|
it.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$animeLocation/$title/$episode"
|
||||||
|
).listFiles()?.forEach {
|
||||||
|
if (it.name.contains("subtitle")) {
|
||||||
|
return listOf(
|
||||||
|
Subtitle(
|
||||||
|
"Downloaded Subtitle",
|
||||||
|
Uri.fromFile(it).toString(),
|
||||||
|
determineSubtitletype(it.absolutePath)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun determineSubtitletype(url: String): SubtitleType {
|
||||||
|
return when {
|
||||||
|
url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
|
||||||
|
url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
|
||||||
|
else -> SubtitleType.SRT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
fun removeMediaCompat(context: Context, title: String, type: MediaType) {
|
||||||
|
val subDirectory = if (type == MediaType.MANGA) {
|
||||||
|
"Manga"
|
||||||
|
} else if (type == MediaType.ANIME) {
|
||||||
|
"Anime"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$subDirectory/$title"
|
||||||
|
)
|
||||||
|
if (directory.exists()) {
|
||||||
|
directory.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("external storage is deprecated, use SAF instead")
|
||||||
|
fun removeDownloadCompat(context: Context, downloadedType: DownloadedType) {
|
||||||
|
val directory = if (downloadedType.type == MediaType.MANGA) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Manga/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||||
|
)
|
||||||
|
} else if (downloadedType.type == MediaType.ANIME) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Anime/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val animeLocation = "Dantotsu/Anime"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,27 @@
|
|||||||
package ani.dantotsu.download
|
package ani.dantotsu.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import ani.dantotsu.download.DownloadCompat.Companion.removeDownloadCompat
|
||||||
|
import ani.dantotsu.download.DownloadCompat.Companion.removeMediaCompat
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import com.anggrayudi.storage.callback.FolderCallback
|
||||||
|
import com.anggrayudi.storage.file.deleteRecursively
|
||||||
|
import com.anggrayudi.storage.file.findFolder
|
||||||
|
import com.anggrayudi.storage.file.moveFolderTo
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import java.io.File
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
class DownloadsManager(private val context: Context) {
|
class DownloadsManager(private val context: Context) {
|
||||||
@@ -15,11 +29,11 @@ class DownloadsManager(private val context: Context) {
|
|||||||
private val downloadsList = loadDownloads().toMutableList()
|
private val downloadsList = loadDownloads().toMutableList()
|
||||||
|
|
||||||
val mangaDownloadedTypes: List<DownloadedType>
|
val mangaDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
get() = downloadsList.filter { it.type == MediaType.MANGA }
|
||||||
val animeDownloadedTypes: List<DownloadedType>
|
val animeDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
get() = downloadsList.filter { it.type == MediaType.ANIME }
|
||||||
val novelDownloadedTypes: List<DownloadedType>
|
val novelDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
get() = downloadsList.filter { it.type == MediaType.NOVEL }
|
||||||
|
|
||||||
private fun saveDownloads() {
|
private fun saveDownloads() {
|
||||||
val jsonString = gson.toJson(downloadsList)
|
val jsonString = gson.toJson(downloadsList)
|
||||||
@@ -41,84 +55,72 @@ class DownloadsManager(private val context: Context) {
|
|||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeDownload(downloadedType: DownloadedType) {
|
fun removeDownload(
|
||||||
|
downloadedType: DownloadedType,
|
||||||
|
toast: Boolean = true,
|
||||||
|
onFinished: () -> Unit
|
||||||
|
) {
|
||||||
|
removeDownloadCompat(context, downloadedType)
|
||||||
downloadsList.remove(downloadedType)
|
downloadsList.remove(downloadedType)
|
||||||
removeDirectory(downloadedType)
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
removeDirectory(downloadedType, toast)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMedia(title: String, type: DownloadedType.Type) {
|
fun removeMedia(title: String, type: MediaType) {
|
||||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
removeMediaCompat(context, title, type)
|
||||||
"Manga"
|
val baseDirectory = getBaseDirectory(context, type)
|
||||||
} else if (type == DownloadedType.Type.ANIME) {
|
val directory = baseDirectory?.findFolder(title)
|
||||||
"Anime"
|
if (directory?.exists() == true) {
|
||||||
} else {
|
val deleted = directory.deleteRecursively(context, false)
|
||||||
"Novel"
|
|
||||||
}
|
|
||||||
val directory = File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/$subDirectory/$title"
|
|
||||||
)
|
|
||||||
if (directory.exists()) {
|
|
||||||
val deleted = directory.deleteRecursively()
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
snackString("Successfully deleted")
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
snackString("Failed to delete directory")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
snackString("Directory does not exist")
|
||||||
cleanDownloads()
|
cleanDownloads()
|
||||||
}
|
}
|
||||||
when (type) {
|
when (type) {
|
||||||
DownloadedType.Type.MANGA -> {
|
MediaType.MANGA -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA }
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadedType.Type.ANIME -> {
|
MediaType.ANIME -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME }
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadedType.Type.NOVEL -> {
|
MediaType.NOVEL -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanDownloads() {
|
private fun cleanDownloads() {
|
||||||
cleanDownload(DownloadedType.Type.MANGA)
|
cleanDownload(MediaType.MANGA)
|
||||||
cleanDownload(DownloadedType.Type.ANIME)
|
cleanDownload(MediaType.ANIME)
|
||||||
cleanDownload(DownloadedType.Type.NOVEL)
|
cleanDownload(MediaType.NOVEL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanDownload(type: DownloadedType.Type) {
|
private fun cleanDownload(type: MediaType) {
|
||||||
// remove all folders that are not in the downloads list
|
// remove all folders that are not in the downloads list
|
||||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
val directory = getBaseDirectory(context, type)
|
||||||
"Manga"
|
val downloadsSubLists = when (type) {
|
||||||
} else if (type == DownloadedType.Type.ANIME) {
|
MediaType.MANGA -> mangaDownloadedTypes
|
||||||
"Anime"
|
MediaType.ANIME -> animeDownloadedTypes
|
||||||
} else {
|
else -> novelDownloadedTypes
|
||||||
"Novel"
|
|
||||||
}
|
}
|
||||||
val directory = File(
|
if (directory?.exists() == true && directory.isDirectory) {
|
||||||
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()
|
val files = directory.listFiles()
|
||||||
if (files != null) {
|
for (file in files) {
|
||||||
for (file in files) {
|
if (!downloadsSubLists.any { it.titleName == file.name }) {
|
||||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
file.deleteRecursively(context, false)
|
||||||
val deleted = file.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,122 +128,129 @@ class DownloadsManager(private val context: Context) {
|
|||||||
val iterator = downloadsList.iterator()
|
val iterator = downloadsList.iterator()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val download = iterator.next()
|
val download = iterator.next()
|
||||||
val downloadDir = File(directory, download.title)
|
val downloadDir = directory?.findFolder(download.titleName)
|
||||||
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
if ((downloadDir?.exists() == false && download.type == type) || download.titleName.isBlank()) {
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
fun moveDownloadsDir(
|
||||||
{
|
context: Context,
|
||||||
val jsonString = gson.toJson(downloadsList)
|
oldUri: Uri,
|
||||||
val file = File(
|
newUri: Uri,
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
finished: (Boolean, String) -> Unit
|
||||||
"Dantotsu/downloads.json"
|
) {
|
||||||
)
|
try {
|
||||||
if (file.parentFile?.exists() == false) {
|
if (oldUri == newUri) {
|
||||||
file.parentFile?.mkdirs()
|
finished(false, "Source and destination are the same")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|
||||||
|
val oldBase =
|
||||||
|
DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
|
||||||
|
val newBase =
|
||||||
|
DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
|
||||||
|
val folder =
|
||||||
|
oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
|
||||||
|
folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object :
|
||||||
|
FolderCallback() {
|
||||||
|
override fun onFailed(errorCode: ErrorCode) {
|
||||||
|
when (errorCode) {
|
||||||
|
ErrorCode.CANCELED -> finished(false, "Move canceled")
|
||||||
|
ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(
|
||||||
|
false,
|
||||||
|
"Cannot create file in target"
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrorCode.INVALID_TARGET_FOLDER -> finished(
|
||||||
|
true,
|
||||||
|
"Invalid target folder"
|
||||||
|
) // seems to still work
|
||||||
|
ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(
|
||||||
|
false,
|
||||||
|
"No space left on target path"
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
|
||||||
|
ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(
|
||||||
|
false,
|
||||||
|
"Source folder not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrorCode.STORAGE_PERMISSION_DENIED -> finished(
|
||||||
|
false,
|
||||||
|
"Storage permission denied"
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(
|
||||||
|
false,
|
||||||
|
"Target folder cannot have same path with source folder"
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> finished(false, "Failed to move downloads: $errorCode")
|
||||||
|
}
|
||||||
|
Logger.log("Failed to move downloads: $errorCode")
|
||||||
|
super.onFailed(errorCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCompleted(result: Result) {
|
||||||
|
finished(true, "Successfully moved downloads")
|
||||||
|
super.onCompleted(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snackString("Error: ${e.message}")
|
||||||
|
finished(false, "Failed to move downloads: ${e.message}")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (!file.exists()) {
|
|
||||||
file.createNewFile()
|
|
||||||
}
|
|
||||||
file.writeText(jsonString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||||
return downloadsList.contains(downloadedType)
|
return downloadsList.contains(downloadedType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
|
||||||
return if (type == null) {
|
return if (type == null) {
|
||||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
downloadsList.any { it.titleName == title && it.chapterName == chapter }
|
||||||
} else {
|
} else {
|
||||||
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
|
downloadsList.any { it.titleName == title && it.chapterName == chapter && it.type == type }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
|
||||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
||||||
File(
|
val directory =
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
baseDirectory?.findFolder(downloadedType.titleName)
|
||||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
?.findFolder(downloadedType.chapterName)
|
||||||
)
|
downloadsList.remove(downloadedType)
|
||||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
|
||||||
File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the directory exists and delete it recursively
|
// Check if the directory exists and delete it recursively
|
||||||
if (directory.exists()) {
|
if (directory?.exists() == true) {
|
||||||
val deleted = directory.deleteRecursively()
|
val deleted = directory.deleteRecursively(context, false)
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
if (toast) snackString("Successfully deleted")
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
snackString("Failed to delete directory")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
snackString("Directory does not exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
fun purgeDownloads(type: MediaType) {
|
||||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
val directory = getBaseDirectory(context, type)
|
||||||
File(
|
if (directory?.exists() == true) {
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
val deleted = directory.deleteRecursively(context, false)
|
||||||
"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) {
|
if (deleted) {
|
||||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
snackString("Successfully deleted")
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
snackString("Failed to delete directory")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
snackString("Directory does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadsList.removeAll { it.type == type }
|
downloadsList.removeAll { it.type == type }
|
||||||
@@ -249,62 +258,132 @@ class DownloadsManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val novelLocation = "Dantotsu/Novel"
|
private const val BASE_LOCATION = "Dantotsu"
|
||||||
const val mangaLocation = "Dantotsu/Manga"
|
private const val MANGA_SUB_LOCATION = "Manga"
|
||||||
const val animeLocation = "Dantotsu/Anime"
|
private const val ANIME_SUB_LOCATION = "Anime"
|
||||||
|
private const val NOVEL_SUB_LOCATION = "Novel"
|
||||||
|
|
||||||
fun getDirectory(
|
|
||||||
context: Context,
|
/**
|
||||||
type: DownloadedType.Type,
|
* Get and create a base directory for the given type
|
||||||
title: String,
|
* @param context the context
|
||||||
chapter: String? = null
|
* @param type the type of media
|
||||||
): File {
|
* @return the base directory
|
||||||
return if (type == DownloadedType.Type.MANGA) {
|
*/
|
||||||
if (chapter != null) {
|
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
|
||||||
File(
|
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
if (baseDirectory == Uri.EMPTY) return null
|
||||||
"$mangaLocation/$title/$chapter"
|
var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
|
||||||
)
|
base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
|
||||||
} else {
|
return when (type) {
|
||||||
File(
|
MediaType.MANGA -> {
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
|
||||||
"$mangaLocation/$title"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else if (type == DownloadedType.Type.ANIME) {
|
|
||||||
if (chapter != null) {
|
MediaType.ANIME -> {
|
||||||
File(
|
base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$animeLocation/$title/$chapter"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$animeLocation/$title"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (chapter != null) {
|
else -> {
|
||||||
File(
|
base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$novelLocation/$title/$chapter"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$novelLocation/$title"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* Get and create a subdirectory for the given type
|
||||||
|
* @param context the context
|
||||||
|
* @param type the type of media
|
||||||
|
* @param title the title of the media
|
||||||
|
* @param chapter the chapter of the media
|
||||||
|
* @return the subdirectory
|
||||||
|
*/
|
||||||
|
fun getSubDirectory(
|
||||||
|
context: Context,
|
||||||
|
type: MediaType,
|
||||||
|
overwrite: Boolean,
|
||||||
|
title: String,
|
||||||
|
chapter: String? = null
|
||||||
|
): DocumentFile? {
|
||||||
|
val baseDirectory = getBaseDirectory(context, type) ?: return null
|
||||||
|
return if (chapter != null) {
|
||||||
|
baseDirectory.findOrCreateFolder(title, false)
|
||||||
|
?.findOrCreateFolder(chapter, overwrite)
|
||||||
|
} else {
|
||||||
|
baseDirectory.findOrCreateFolder(title, overwrite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
fun getDirSize(
|
||||||
enum class Type {
|
context: Context,
|
||||||
MANGA,
|
type: MediaType,
|
||||||
ANIME,
|
title: String,
|
||||||
NOVEL
|
chapter: String? = null
|
||||||
|
): Long {
|
||||||
|
val directory = getSubDirectory(context, type, false, title, chapter) ?: return 0
|
||||||
|
var size = 0L
|
||||||
|
directory.listFiles().forEach {
|
||||||
|
size += it.length()
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addNoMedia(context: Context) {
|
||||||
|
val baseDirectory = getBaseDirectory(context) ?: return
|
||||||
|
if (baseDirectory.findFile(".nomedia") == null) {
|
||||||
|
baseDirectory.createFile("application/octet-stream", ".nomedia")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseDirectory(context: Context): DocumentFile? {
|
||||||
|
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
|
||||||
|
if (baseDirectory == Uri.EMPTY) return null
|
||||||
|
return DocumentFile.fromTreeUri(context, baseDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DocumentFile.findOrCreateFolder(
|
||||||
|
name: String, overwrite: Boolean
|
||||||
|
): DocumentFile? {
|
||||||
|
return if (overwrite) {
|
||||||
|
findFolder(name.findValidName())?.delete()
|
||||||
|
createDirectory(name.findValidName())
|
||||||
|
} else {
|
||||||
|
findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val RATIO_THRESHOLD = 95
|
||||||
|
fun Media.compareName(name: String): Boolean {
|
||||||
|
val mainName = mainName().findValidName().lowercase()
|
||||||
|
val ratio = FuzzySearch.ratio(mainName, name.lowercase())
|
||||||
|
return ratio > RATIO_THRESHOLD
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.compareName(name: String): Boolean {
|
||||||
|
val mainName = findValidName().lowercase()
|
||||||
|
val compareName = name.findValidName().lowercase()
|
||||||
|
val ratio = FuzzySearch.ratio(mainName, compareName)
|
||||||
|
return ratio > RATIO_THRESHOLD
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
|
||||||
|
fun String?.findValidName(): String {
|
||||||
|
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DownloadedType(
|
||||||
|
private val pTitle: String?,
|
||||||
|
private val pChapter: String?,
|
||||||
|
val type: MediaType,
|
||||||
|
@Deprecated("use pTitle instead")
|
||||||
|
private val title: String? = null,
|
||||||
|
@Deprecated("use pChapter instead")
|
||||||
|
private val chapter: String? = null
|
||||||
|
) : Serializable {
|
||||||
|
val titleName: String
|
||||||
|
get() = title?:pTitle.findValidName()
|
||||||
|
val chapterName: String
|
||||||
|
get() = chapter?:pChapter.findValidName()
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,32 +9,35 @@ import android.content.IntentFilter
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.media3.common.util.UnstableApi
|
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.FileUrl
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.currActivity
|
import ani.dantotsu.defaultHeaders
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.download.video.Helper
|
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.SubtitleDownloader
|
import ani.dantotsu.media.SubtitleDownloader
|
||||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
import ani.dantotsu.parsers.Video
|
import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.toast
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import com.anggrayudi.storage.file.forceDelete
|
||||||
|
import com.anggrayudi.storage.file.openOutputStream
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
@@ -45,25 +48,21 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import tachiyomi.core.util.lang.launchIO
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
class AnimeDownloaderService : Service() {
|
class AnimeDownloaderService : Service() {
|
||||||
|
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
@@ -74,6 +73,7 @@ class AnimeDownloaderService : Service() {
|
|||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private var isCurrentlyProcessing = false
|
private var isCurrentlyProcessing = false
|
||||||
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
|
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
|
||||||
|
private val ffExtension = Injekt.get<DownloadAddonManager>().extension?.extension
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
// This is only required for bound services.
|
// This is only required for bound services.
|
||||||
@@ -82,6 +82,11 @@ class AnimeDownloaderService : Service() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
if (ffExtension == null) {
|
||||||
|
toast(getString(R.string.download_addon_not_found))
|
||||||
|
stopSelf()
|
||||||
|
return
|
||||||
|
}
|
||||||
notificationManager = NotificationManagerCompat.from(this)
|
notificationManager = NotificationManagerCompat.from(this)
|
||||||
builder =
|
builder =
|
||||||
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||||
@@ -89,6 +94,7 @@ class AnimeDownloaderService : Service() {
|
|||||||
setSmallIcon(R.drawable.ic_download_24)
|
setSmallIcon(R.drawable.ic_download_24)
|
||||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
|
setProgress(100, 0, false)
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
startForeground(
|
startForeground(
|
||||||
@@ -157,27 +163,14 @@ class AnimeDownloaderService : Service() {
|
|||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun cancelDownload(taskName: String) {
|
fun cancelDownload(taskName: String) {
|
||||||
val url =
|
val sessionIds =
|
||||||
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
|
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
|
||||||
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
.map { it.sessionId }.toMutableList()
|
||||||
if (url.isEmpty()) {
|
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
||||||
snackString("Failed to cancel download")
|
sessionIds.forEach {
|
||||||
return
|
ffExtension!!.cancelDownload(it)
|
||||||
}
|
}
|
||||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
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 {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
downloadJobs[taskName]?.cancel()
|
downloadJobs[taskName]?.cancel()
|
||||||
@@ -210,7 +203,6 @@ class AnimeDownloaderService : Service() {
|
|||||||
@androidx.annotation.OptIn(UnstableApi::class)
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
suspend fun download(task: AnimeDownloadTask) {
|
suspend fun download(task: AnimeDownloadTask) {
|
||||||
try {
|
try {
|
||||||
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
@@ -221,18 +213,63 @@ class AnimeDownloaderService : Service() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
|
||||||
if (notifi) {
|
if (notifi) {
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
currActivity()?.let {
|
val outputDir = getSubDirectory(
|
||||||
Helper.downloadVideo(
|
this@AnimeDownloaderService,
|
||||||
it,
|
MediaType.ANIME,
|
||||||
task.video,
|
false,
|
||||||
task.subtitle
|
task.title,
|
||||||
)
|
task.episode
|
||||||
|
) ?: throw Exception("Failed to create output directory")
|
||||||
|
|
||||||
|
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
|
||||||
|
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
|
||||||
|
?: throw Exception("Failed to create output file")
|
||||||
|
|
||||||
|
var percent = 0
|
||||||
|
var totalLength = 0.0
|
||||||
|
val path = ffExtension!!.setDownloadPath(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
outputFile.uri
|
||||||
|
)
|
||||||
|
val headersStringBuilder = StringBuilder()
|
||||||
|
task.video.file.headers.forEach {
|
||||||
|
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
|
||||||
}
|
}
|
||||||
|
if (!task.video.file.headers.containsKey("User-Agent")) { //headers should never be empty now
|
||||||
|
headersStringBuilder.append("\"").append("User-Agent: ")
|
||||||
|
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
|
||||||
|
}
|
||||||
|
val probeRequest =
|
||||||
|
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\""
|
||||||
|
ffExtension.executeFFProbe(
|
||||||
|
probeRequest
|
||||||
|
) {
|
||||||
|
if (it.toDoubleOrNull() != null) {
|
||||||
|
totalLength = it.toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val headers = headersStringBuilder.toString()
|
||||||
|
var request = "-headers $headers "
|
||||||
|
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
|
||||||
|
Logger.log("Request: $request")
|
||||||
|
val ffTask =
|
||||||
|
ffExtension.executeFFMpeg(request) {
|
||||||
|
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||||
|
val timeInMilliseconds = it
|
||||||
|
if (timeInMilliseconds > 0 && totalLength > 0) {
|
||||||
|
percent = ((it / 1000) / totalLength * 100).toInt()
|
||||||
|
}
|
||||||
|
Logger.log("Statistics: $it")
|
||||||
|
}
|
||||||
|
task.sessionId = ffTask
|
||||||
|
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
|
||||||
|
ffTask
|
||||||
|
|
||||||
saveMediaInfo(task)
|
saveMediaInfo(task)
|
||||||
task.subtitle?.let {
|
task.subtitle?.let {
|
||||||
@@ -242,90 +279,120 @@ class AnimeDownloaderService : Service() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.episode,
|
task.episode,
|
||||||
DownloadedType.Type.ANIME,
|
MediaType.ANIME,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val downloadStarted =
|
|
||||||
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
|
|
||||||
|
|
||||||
if (!downloadStarted) {
|
|
||||||
Logger.log("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
|
// periodically check if the download is complete
|
||||||
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
||||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
if (ffExtension.getState(ffTask) == "FAILED") {
|
||||||
if (download != null) {
|
Logger.log("Download failed")
|
||||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
builder.setContentText(
|
||||||
Logger.log("Download failed")
|
"${
|
||||||
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
getTaskName(
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
|
||||||
snackString("${task.title} - ${task.episode} Download failed")
|
|
||||||
Logger.log("Download failed: ${download.failureReason}")
|
|
||||||
downloadsManager.removeDownload(
|
|
||||||
DownloadedType(
|
|
||||||
task.title,
|
task.title,
|
||||||
task.episode,
|
task.episode
|
||||||
DownloadedType.Type.ANIME,
|
|
||||||
)
|
)
|
||||||
)
|
} Download failed"
|
||||||
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.log("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.log("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())
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
toast("${getTaskName(task.title, task.episode)} Download failed")
|
||||||
}
|
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
|
||||||
|
downloadsManager.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.episode,
|
||||||
|
MediaType.ANIME,
|
||||||
|
),
|
||||||
|
false
|
||||||
|
) {}
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(
|
||||||
|
Exception(
|
||||||
|
"Anime Download failed:" +
|
||||||
|
" ${getTaskName(task.title, task.episode)}" +
|
||||||
|
" url: ${task.video.file.url}" +
|
||||||
|
" title: ${task.title}" +
|
||||||
|
" episode: ${task.episode}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
|
broadcastDownloadFailed(task.episode)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
builder.setProgress(
|
||||||
|
100, percent.coerceAtMost(99),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
broadcastDownloadProgress(
|
||||||
|
task.episode,
|
||||||
|
percent.coerceAtMost(99)
|
||||||
|
)
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
kotlinx.coroutines.delay(2000)
|
kotlinx.coroutines.delay(2000)
|
||||||
}
|
}
|
||||||
|
if (ffExtension.getState(ffTask) == "COMPLETED") {
|
||||||
|
if (ffExtension.hadError(ffTask)) {
|
||||||
|
Logger.log("Download failed")
|
||||||
|
builder.setContentText(
|
||||||
|
"${
|
||||||
|
getTaskName(
|
||||||
|
task.title,
|
||||||
|
task.episode
|
||||||
|
)
|
||||||
|
} Download failed"
|
||||||
|
)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${getTaskName(task.title, task.episode)} Download failed")
|
||||||
|
downloadsManager.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.episode,
|
||||||
|
MediaType.ANIME,
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(
|
||||||
|
Exception(
|
||||||
|
"Anime Download failed:" +
|
||||||
|
" ${getTaskName(task.title, task.episode)}" +
|
||||||
|
" url: ${task.video.file.url}" +
|
||||||
|
" title: ${task.title}" +
|
||||||
|
" episode: ${task.episode}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
|
broadcastDownloadFailed(task.episode)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
Logger.log("Download completed")
|
||||||
|
builder.setContentText(
|
||||||
|
"${
|
||||||
|
getTaskName(
|
||||||
|
task.title,
|
||||||
|
task.episode
|
||||||
|
)
|
||||||
|
} Download completed"
|
||||||
|
)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${getTaskName(task.title, task.episode)} Download completed")
|
||||||
|
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||||
|
task.getTaskName(),
|
||||||
|
task.video.file.url
|
||||||
|
).apply()
|
||||||
|
downloadsManager.addDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.episode,
|
||||||
|
MediaType.ANIME,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
|
broadcastDownloadFinished(task.episode)
|
||||||
|
} else throw Exception("Download failed")
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
||||||
@@ -338,34 +405,24 @@ class AnimeDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||||
launchIO {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val directory = File(
|
val directory =
|
||||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
|
||||||
"${DownloadsManager.animeLocation}/${task.title}"
|
?: throw Exception("Directory not found")
|
||||||
)
|
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
|
||||||
val episodeDirectory = File(directory, task.episode)
|
val file = directory.createFile("application/json", "media.json")
|
||||||
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
?: throw Exception("File not created")
|
||||||
|
val episodeDirectory =
|
||||||
|
getSubDirectory(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
MediaType.ANIME,
|
||||||
|
false,
|
||||||
|
task.title,
|
||||||
|
task.episode
|
||||||
|
)
|
||||||
|
?: throw Exception("Directory not found")
|
||||||
|
|
||||||
val file = File(directory, "media.json")
|
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
@@ -399,14 +456,25 @@ class AnimeDownloaderService : Service() {
|
|||||||
|
|
||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
file.writeText(jsonString)
|
try {
|
||||||
|
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
|
||||||
|
if (output == null) throw Exception("Output stream is null")
|
||||||
|
output.write(jsonString.toByteArray())
|
||||||
|
}
|
||||||
|
} catch (e: android.system.ErrnoException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Toast.makeText(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
"Error while saving: ${e.localizedMessage}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
var connection: HttpURLConnection? = null
|
var connection: HttpURLConnection? = null
|
||||||
println("Downloading url $url")
|
println("Downloading url $url")
|
||||||
@@ -417,13 +485,16 @@ class AnimeDownloaderService : Service() {
|
|||||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = File(directory, name)
|
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
|
||||||
FileOutputStream(file).use { output ->
|
val file =
|
||||||
|
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||||
|
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
|
||||||
|
if (output == null) throw Exception("Output stream is null")
|
||||||
connection.inputStream.use { input ->
|
connection.inputStream.use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@withContext file.absolutePath
|
return@withContext file.uri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -490,14 +561,15 @@ class AnimeDownloaderService : Service() {
|
|||||||
val episodeImage: String? = null,
|
val episodeImage: String? = null,
|
||||||
val retries: Int = 2,
|
val retries: Int = 2,
|
||||||
val simultaneousDownloads: Int = 2,
|
val simultaneousDownloads: Int = 2,
|
||||||
|
var sessionId: Long = -1
|
||||||
) {
|
) {
|
||||||
fun getTaskName(): String {
|
fun getTaskName(): String {
|
||||||
return "$title - $episode"
|
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getTaskName(title: String, episode: String): String {
|
fun getTaskName(title: String, episode: String): String {
|
||||||
return "$title - $episode"
|
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,7 +583,6 @@ class AnimeDownloaderService : Service() {
|
|||||||
|
|
||||||
object AnimeServiceDataSingleton {
|
object AnimeServiceDataSingleton {
|
||||||
var video: Video? = null
|
var video: Video? = null
|
||||||
var sourceMedia: Media? = null
|
|
||||||
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package ani.dantotsu.download.anime
|
package ani.dantotsu.download.anime
|
||||||
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -38,7 +37,6 @@ class OfflineAnimeAdapter(
|
|||||||
return position.toLong()
|
return position.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||||
|
|
||||||
val view: View = convertView ?: when (style) {
|
val view: View = convertView ?: when (style) {
|
||||||
@@ -51,28 +49,27 @@ class OfflineAnimeAdapter(
|
|||||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
|
||||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||||
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
|
val totalEpisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||||
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||||
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
||||||
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
||||||
|
|
||||||
if (style == 0) {
|
if (style == 0) {
|
||||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||||
val episodes = view.findViewById<TextView>(R.id.itemTotal)
|
val episodes = view.findViewById<TextView>(R.id.itemTotal)
|
||||||
episodes.text = " Episodes"
|
episodes.text = context.getString(R.string.episodes)
|
||||||
bannerView.setImageURI(item.banner)
|
bannerView.setImageURI(item.banner ?: item.image)
|
||||||
totalepisodes.text = item.totalEpisodeList
|
totalEpisodes.text = item.totalEpisodeList
|
||||||
} else if (style == 1) {
|
} else if (style == 1) {
|
||||||
val watchedEpisodes =
|
val watchedEpisodes =
|
||||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||||
watchedEpisodes.text = item.watchedEpisode
|
watchedEpisodes.text = item.watchedEpisode
|
||||||
totalepisodes.text = " | " + item.totalEpisode
|
totalEpisodes.text = context.getString(R.string.total_divider, item.totalEpisode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind item data to the views
|
// Bind item data to the views
|
||||||
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
|
typeImage.setImageResource(R.drawable.ic_round_movie_filter_24)
|
||||||
type.text = item.type
|
type.text = item.type
|
||||||
typeView.visibility = View.VISIBLE
|
typeView.visibility = View.VISIBLE
|
||||||
imageView.setImageURI(item.image)
|
imageView.setImageURI(item.image)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package ani.dantotsu.download.anime
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
@@ -22,26 +21,34 @@ import androidx.annotation.OptIn
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginBottom
|
import androidx.core.view.marginBottom
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.bottomBar
|
import ani.dantotsu.bottomBar
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.currActivity
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
|
||||||
|
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||||
|
import ani.dantotsu.download.findValidName
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import com.anggrayudi.storage.file.openInputStream
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
@@ -53,9 +60,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
|
|||||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
|
|
||||||
@@ -64,6 +75,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
private lateinit var gridView: GridView
|
private lateinit var gridView: GridView
|
||||||
private lateinit var adapter: OfflineAnimeAdapter
|
private lateinit var adapter: OfflineAnimeAdapter
|
||||||
private lateinit var total: TextView
|
private lateinit var total: TextView
|
||||||
|
private var downloadsJob: Job = Job()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -110,10 +122,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
})
|
})
|
||||||
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
val layoutCompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||||
var selected = when (style) {
|
var selected = when (style) {
|
||||||
0 -> layoutList
|
0 -> layoutList
|
||||||
1 -> layoutcompact
|
1 -> layoutCompact
|
||||||
else -> layoutList
|
else -> layoutList
|
||||||
}
|
}
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
@@ -134,7 +146,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
grid()
|
grid()
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutcompact.setOnClickListener {
|
layoutCompact.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageView)
|
||||||
style = 1
|
style = 1
|
||||||
PrefManager.setVal(PrefName.OfflineView, style)
|
PrefManager.setVal(PrefName.OfflineView, style)
|
||||||
@@ -154,11 +166,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun grid() {
|
private fun grid() {
|
||||||
gridView.visibility = View.VISIBLE
|
gridView.visibility = View.VISIBLE
|
||||||
getDownloads()
|
|
||||||
val fadeIn = AlphaAnimation(0f, 1f)
|
val fadeIn = AlphaAnimation(0f, 1f)
|
||||||
fadeIn.duration = 300 // animations pog
|
fadeIn.duration = 300 // animations pog
|
||||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||||
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
||||||
|
getDownloads()
|
||||||
gridView.adapter = adapter
|
gridView.adapter = adapter
|
||||||
gridView.scheduleLayoutAnimation()
|
gridView.scheduleLayoutAnimation()
|
||||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||||
@@ -166,20 +178,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
// Get the OfflineAnimeModel that was clicked
|
// Get the OfflineAnimeModel that was clicked
|
||||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||||
val media =
|
val media =
|
||||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
|
downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||||
media?.let {
|
media?.let {
|
||||||
val mediaModel = getMedia(it)
|
lifecycleScope.launch {
|
||||||
if (mediaModel == null) {
|
val mediaModel = getMedia(it)
|
||||||
snackString("Error loading media.json")
|
if (mediaModel == null) {
|
||||||
return@let
|
snackString("Error loading media.json")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||||
|
ContextCompat.startActivity(
|
||||||
|
requireActivity(),
|
||||||
|
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||||
|
.putExtra("download", true),
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
|
||||||
ContextCompat.startActivity(
|
|
||||||
requireActivity(),
|
|
||||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
|
||||||
.putExtra("download", true),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
snackString("no media found")
|
snackString("no media found")
|
||||||
}
|
}
|
||||||
@@ -187,8 +201,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
// Get the OfflineAnimeModel that was clicked
|
// Get the OfflineAnimeModel that was clicked
|
||||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||||
val type: DownloadedType.Type =
|
val type: MediaType = MediaType.ANIME
|
||||||
DownloadedType.Type.ANIME
|
|
||||||
|
|
||||||
// Alert dialog to confirm deletion
|
// Alert dialog to confirm deletion
|
||||||
val builder =
|
val builder =
|
||||||
@@ -203,13 +216,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
if (mediaIds.isEmpty()) {
|
if (mediaIds.isEmpty()) {
|
||||||
snackString("No media found") // if this happens, terrible things have happened
|
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()
|
getDownloads()
|
||||||
adapter.setItems(downloads)
|
|
||||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
|
||||||
}
|
}
|
||||||
builder.setNegativeButton("No") { _, _ ->
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
// Do nothing
|
// Do nothing
|
||||||
@@ -237,7 +244,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
|
|
||||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||||
// Implement behavior for different scroll states if needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
@@ -250,7 +256,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
val visibility = first != null && first.top < 0
|
val visibility = first != null && first.top < 0
|
||||||
scrollTop.translationY =
|
scrollTop.translationY =
|
||||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
scrollTop.isVisible = visibility
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
initActivity(requireActivity())
|
initActivity(requireActivity())
|
||||||
@@ -260,7 +266,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
getDownloads()
|
getDownloads()
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -280,29 +285,39 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
|
|
||||||
private fun getDownloads() {
|
private fun getDownloads() {
|
||||||
downloads = listOf()
|
downloads = listOf()
|
||||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
if (downloadsJob.isActive) {
|
||||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
downloadsJob.cancel()
|
||||||
for (title in animeTitles) {
|
}
|
||||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
downloadsJob = Job()
|
||||||
val download = tDownloads.first()
|
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||||
newAnimeDownloads += offlineAnimeModel
|
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||||
|
for (title in animeTitles) {
|
||||||
|
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName == title }
|
||||||
|
val download = tDownloads.first()
|
||||||
|
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||||
|
newAnimeDownloads += offlineAnimeModel
|
||||||
|
}
|
||||||
|
downloads = newAnimeDownloads
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
adapter.setItems(downloads)
|
||||||
|
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
downloads = newAnimeDownloads
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
/**
|
||||||
val type = when (downloadedType.type) {
|
* Load media.json file from the directory and convert it to Media class
|
||||||
DownloadedType.Type.MANGA -> "Manga"
|
* @param downloadedType DownloadedType object
|
||||||
DownloadedType.Type.ANIME -> "Anime"
|
* @return Media object
|
||||||
else -> "Novel"
|
*/
|
||||||
}
|
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
val directory = File(
|
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
|
||||||
)
|
|
||||||
//load media.json and convert to media class with gson
|
|
||||||
return try {
|
return try {
|
||||||
|
val directory = DownloadsManager.getSubDirectory(
|
||||||
|
context ?: currContext()!!, downloadedType.type,
|
||||||
|
false, downloadedType.titleName
|
||||||
|
)
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
@@ -314,8 +329,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||||
})
|
})
|
||||||
.create()
|
.create()
|
||||||
val media = File(directory, "media.json")
|
val media = directory?.findFile("media.json")
|
||||||
val mediaJson = media.readText()
|
?: return loadMediaCompat(downloadedType)
|
||||||
|
val mediaJson =
|
||||||
|
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||||
|
it?.readText()
|
||||||
|
}
|
||||||
|
?: return null
|
||||||
gson.fromJson(mediaJson, Media::class.java)
|
gson.fromJson(mediaJson, Media::class.java)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error loading media.json: ${e.message}")
|
Logger.log("Error loading media.json: ${e.message}")
|
||||||
@@ -325,27 +345,28 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
/**
|
||||||
val type = when (downloadedType.type) {
|
* Load OfflineAnimeModel from the directory
|
||||||
DownloadedType.Type.MANGA -> "Manga"
|
* @param downloadedType DownloadedType object
|
||||||
DownloadedType.Type.ANIME -> "Anime"
|
* @return OfflineAnimeModel object
|
||||||
else -> "Novel"
|
*/
|
||||||
}
|
private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||||
val directory = File(
|
val type = downloadedType.type.asText()
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
|
||||||
)
|
|
||||||
//load media.json and convert to media class with gson
|
|
||||||
try {
|
try {
|
||||||
|
val directory = DownloadsManager.getSubDirectory(
|
||||||
|
context ?: currContext()!!, downloadedType.type,
|
||||||
|
false, downloadedType.titleName
|
||||||
|
)
|
||||||
val mediaModel = getMedia(downloadedType)!!
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
val cover = File(directory, "cover.jpg")
|
val cover = directory?.findFile("cover.jpg")
|
||||||
val coverUri: Uri? = if (cover.exists()) {
|
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||||
Uri.fromFile(cover)
|
cover.uri
|
||||||
} else null
|
} else null
|
||||||
val banner = File(directory, "banner.jpg")
|
val banner = directory?.findFile("banner.jpg")
|
||||||
val bannerUri: Uri? = if (banner.exists()) {
|
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||||
Uri.fromFile(banner)
|
banner.uri
|
||||||
} else null
|
} else null
|
||||||
|
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
|
||||||
val title = mediaModel.mainName()
|
val title = mediaModel.mainName()
|
||||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||||
@@ -374,22 +395,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
bannerUri
|
bannerUri
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error loading media.json: ${e.message}")
|
return try {
|
||||||
Logger.log(e)
|
loadOfflineAnimeModelCompat(downloadedType)
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
} catch (e: Exception) {
|
||||||
return OfflineAnimeModel(
|
Logger.log("Error loading media.json: ${e.message}")
|
||||||
"unknown",
|
Logger.log(e)
|
||||||
"0",
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
"??",
|
OfflineAnimeModel(
|
||||||
"??",
|
"unknown",
|
||||||
"??",
|
"0",
|
||||||
"movie",
|
"??",
|
||||||
"hmm",
|
"??",
|
||||||
isOngoing = false,
|
"??",
|
||||||
isUserScored = false,
|
"movie",
|
||||||
null,
|
"hmm",
|
||||||
null
|
isOngoing = false,
|
||||||
)
|
isUserScored = false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,20 @@ import android.content.pm.PackageManager
|
|||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.manga.ImageData
|
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_FAILED
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||||
@@ -30,6 +31,10 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
|
|||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import com.anggrayudi.storage.file.deleteRecursively
|
||||||
|
import com.anggrayudi.storage.file.forceDelete
|
||||||
|
import com.anggrayudi.storage.file.openOutputStream
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||||
@@ -39,7 +44,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -51,8 +55,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
@@ -189,13 +191,20 @@ class MangaDownloaderService : Service() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
|
||||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||||
if (notifi) {
|
if (notifi) {
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSubDirectory(
|
||||||
|
this@MangaDownloaderService,
|
||||||
|
MediaType.MANGA,
|
||||||
|
false,
|
||||||
|
task.title,
|
||||||
|
task.chapter
|
||||||
|
)?.deleteRecursively(this@MangaDownloaderService)
|
||||||
|
|
||||||
// Loop through each ImageData object from the task
|
// Loop through each ImageData object from the task
|
||||||
var farthest = 0
|
var farthest = 0
|
||||||
for ((index, image) in task.imageData.withIndex()) {
|
for ((index, image) in task.imageData.withIndex()) {
|
||||||
@@ -211,8 +220,7 @@ class MangaDownloaderService : Service() {
|
|||||||
while (bitmap == null && retryCount < task.retries) {
|
while (bitmap == null && retryCount < task.retries) {
|
||||||
bitmap = image.fetchAndProcessImage(
|
bitmap = image.fetchAndProcessImage(
|
||||||
image.page,
|
image.page,
|
||||||
image.source,
|
image.source
|
||||||
this@MangaDownloaderService
|
|
||||||
)
|
)
|
||||||
retryCount++
|
retryCount++
|
||||||
}
|
}
|
||||||
@@ -246,7 +254,7 @@ class MangaDownloaderService : Service() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.chapter,
|
task.chapter,
|
||||||
DownloadedType.Type.MANGA
|
MediaType.MANGA
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
broadcastDownloadFinished(task.chapter)
|
broadcastDownloadFinished(task.chapter)
|
||||||
@@ -264,24 +272,18 @@ class MangaDownloaderService : Service() {
|
|||||||
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||||
try {
|
try {
|
||||||
// Define the directory within the private external storage space
|
// Define the directory within the private external storage space
|
||||||
val directory = File(
|
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
|
||||||
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
?: throw Exception("Directory not found")
|
||||||
"Dantotsu/Manga/$title/$chapter"
|
directory.findFile(fileName)?.forceDelete(this)
|
||||||
)
|
// Create a file reference within that directory for the image
|
||||||
|
val file =
|
||||||
if (!directory.exists()) {
|
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
|
||||||
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
|
// Use a FileOutputStream to write the bitmap to the file
|
||||||
FileOutputStream(file).use { outputStream ->
|
file.openOutputStream(this, false).use { outputStream ->
|
||||||
|
if (outputStream == null) throw Exception("Output stream is null")
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Exception while saving image: ${e.message}")
|
println("Exception while saving image: ${e.message}")
|
||||||
snackString("Exception while saving image: ${e.message}")
|
snackString("Exception while saving image: ${e.message}")
|
||||||
@@ -292,13 +294,12 @@ class MangaDownloaderService : Service() {
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
private fun saveMediaInfo(task: DownloadTask) {
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
launchIO {
|
launchIO {
|
||||||
val directory = File(
|
val directory =
|
||||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
|
||||||
"Dantotsu/Manga/${task.title}"
|
?: throw Exception("Directory not found")
|
||||||
)
|
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
|
||||||
if (!directory.exists()) directory.mkdirs()
|
val file = directory.createFile("application/json", "media.json")
|
||||||
|
?: throw Exception("File not created")
|
||||||
val file = File(directory, "media.json")
|
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
@@ -313,7 +314,10 @@ class MangaDownloaderService : Service() {
|
|||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
file.writeText(jsonString)
|
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
||||||
|
if (output == null) throw Exception("Output stream is null")
|
||||||
|
output.write(jsonString.toByteArray())
|
||||||
|
}
|
||||||
} catch (e: android.system.ErrnoException) {
|
} catch (e: android.system.ErrnoException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
@@ -328,7 +332,7 @@ class MangaDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
var connection: HttpURLConnection? = null
|
var connection: HttpURLConnection? = null
|
||||||
println("Downloading url $url")
|
println("Downloading url $url")
|
||||||
@@ -338,14 +342,16 @@ class MangaDownloaderService : Service() {
|
|||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
}
|
}
|
||||||
|
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
|
||||||
val file = File(directory, name)
|
val file =
|
||||||
FileOutputStream(file).use { output ->
|
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||||
|
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
||||||
|
if (output == null) throw Exception("Output stream is null")
|
||||||
connection.inputStream.use { input ->
|
connection.inputStream.use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@withContext file.absolutePath
|
return@withContext file.uri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.download.manga
|
package ani.dantotsu.download.manga
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -37,7 +36,6 @@ class OfflineMangaAdapter(
|
|||||||
return position.toLong()
|
return position.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||||
|
|
||||||
val view: View = convertView ?: when (style) {
|
val view: View = convertView ?: when (style) {
|
||||||
@@ -50,7 +48,6 @@ class OfflineMangaAdapter(
|
|||||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
|
||||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||||
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
|
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||||
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||||
@@ -60,14 +57,14 @@ class OfflineMangaAdapter(
|
|||||||
if (style == 0) {
|
if (style == 0) {
|
||||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||||
val chapters = view.findViewById<TextView>(R.id.itemTotal)
|
val chapters = view.findViewById<TextView>(R.id.itemTotal)
|
||||||
chapters.text = " Chapters"
|
chapters.text = context.getString(R.string.chapters)
|
||||||
bannerView.setImageURI(item.banner)
|
bannerView.setImageURI(item.banner ?: item.image)
|
||||||
totalChapter.text = item.totalChapter
|
totalChapter.text = item.totalChapter
|
||||||
} else if (style == 1) {
|
} else if (style == 1) {
|
||||||
val readChapter =
|
val readChapter =
|
||||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||||
readChapter.text = item.readChapter
|
readChapter.text = item.readChapter
|
||||||
totalChapter.text = " | " + item.totalChapter
|
totalChapter.text = context.getString(R.string.total_divider, item.totalChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind item data to the views
|
// Bind item data to the views
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package ani.dantotsu.download.manga
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
@@ -20,25 +19,34 @@ import android.widget.TextView
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginBottom
|
import androidx.core.view.marginBottom
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.bottomBar
|
import ani.dantotsu.bottomBar
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.currActivity
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.download.DownloadCompat
|
||||||
|
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
|
import ani.dantotsu.download.findValidName
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import com.anggrayudi.storage.file.openInputStream
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
@@ -46,9 +54,13 @@ import com.google.gson.GsonBuilder
|
|||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
|
|
||||||
@@ -57,6 +69,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
private lateinit var gridView: GridView
|
private lateinit var gridView: GridView
|
||||||
private lateinit var adapter: OfflineMangaAdapter
|
private lateinit var adapter: OfflineMangaAdapter
|
||||||
private lateinit var total: TextView
|
private lateinit var total: TextView
|
||||||
|
private var downloadsJob: Job = Job()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -146,11 +159,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
|
|
||||||
private fun grid() {
|
private fun grid() {
|
||||||
gridView.visibility = View.VISIBLE
|
gridView.visibility = View.VISIBLE
|
||||||
getDownloads()
|
|
||||||
val fadeIn = AlphaAnimation(0f, 1f)
|
val fadeIn = AlphaAnimation(0f, 1f)
|
||||||
fadeIn.duration = 300 // animations pog
|
fadeIn.duration = 300 // animations pog
|
||||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||||
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||||
|
getDownloads()
|
||||||
gridView.adapter = adapter
|
gridView.adapter = adapter
|
||||||
gridView.scheduleLayoutAnimation()
|
gridView.scheduleLayoutAnimation()
|
||||||
total.text =
|
total.text =
|
||||||
@@ -159,17 +172,18 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
// Get the OfflineMangaModel that was clicked
|
// Get the OfflineMangaModel that was clicked
|
||||||
val item = adapter.getItem(position) as OfflineMangaModel
|
val item = adapter.getItem(position) as OfflineMangaModel
|
||||||
val media =
|
val media =
|
||||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||||
media?.let {
|
media?.let {
|
||||||
|
lifecycleScope.launch {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||||
.putExtra("media", getMedia(it))
|
.putExtra("media", getMedia(it))
|
||||||
.putExtra("download", true),
|
.putExtra("download", true),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
snackString("no media found")
|
snackString("no media found")
|
||||||
}
|
}
|
||||||
@@ -178,11 +192,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
// Get the OfflineMangaModel that was clicked
|
// Get the OfflineMangaModel that was clicked
|
||||||
val item = adapter.getItem(position) as OfflineMangaModel
|
val item = adapter.getItem(position) as OfflineMangaModel
|
||||||
val type: DownloadedType.Type =
|
val type: MediaType =
|
||||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) {
|
||||||
DownloadedType.Type.MANGA
|
MediaType.MANGA
|
||||||
} else {
|
} else {
|
||||||
DownloadedType.Type.NOVEL
|
MediaType.NOVEL
|
||||||
}
|
}
|
||||||
// Alert dialog to confirm deletion
|
// Alert dialog to confirm deletion
|
||||||
val builder =
|
val builder =
|
||||||
@@ -192,9 +206,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
builder.setPositiveButton("Yes") { _, _ ->
|
builder.setPositiveButton("Yes") { _, _ ->
|
||||||
downloadManager.removeMedia(item.title, type)
|
downloadManager.removeMedia(item.title, type)
|
||||||
getDownloads()
|
getDownloads()
|
||||||
adapter.setItems(downloads)
|
|
||||||
total.text =
|
|
||||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
|
||||||
}
|
}
|
||||||
builder.setNegativeButton("No") { _, _ ->
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
// Do nothing
|
// Do nothing
|
||||||
@@ -223,7 +234,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
|
|
||||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||||
// Implement behavior for different scroll states if needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
@@ -234,7 +244,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
) {
|
) {
|
||||||
val first = view.getChildAt(0)
|
val first = view.getChildAt(0)
|
||||||
val visibility = first != null && first.top < 0
|
val visibility = first != null && first.top < 0
|
||||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
scrollTop.isVisible = visibility
|
||||||
scrollTop.translationY =
|
scrollTop.translationY =
|
||||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
}
|
}
|
||||||
@@ -246,7 +256,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
getDownloads()
|
getDownloads()
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -266,46 +275,62 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
|
|
||||||
private fun getDownloads() {
|
private fun getDownloads() {
|
||||||
downloads = listOf()
|
downloads = listOf()
|
||||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
if (downloadsJob.isActive) {
|
||||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
downloadsJob.cancel()
|
||||||
for (title in mangaTitles) {
|
|
||||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
|
||||||
val download = tDownloads.first()
|
|
||||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
|
||||||
newMangaDownloads += offlineMangaModel
|
|
||||||
}
|
}
|
||||||
downloads = newMangaDownloads
|
downloads = listOf()
|
||||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
downloadsJob = Job()
|
||||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||||
for (title in novelTitles) {
|
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
val download = tDownloads.first()
|
for (title in mangaTitles) {
|
||||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName == title }
|
||||||
newNovelDownloads += offlineMangaModel
|
val download = tDownloads.first()
|
||||||
|
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||||
|
newMangaDownloads += offlineMangaModel
|
||||||
|
}
|
||||||
|
downloads = newMangaDownloads
|
||||||
|
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
|
||||||
|
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
|
for (title in novelTitles) {
|
||||||
|
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName == title }
|
||||||
|
val download = tDownloads.first()
|
||||||
|
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||||
|
newNovelDownloads += offlineMangaModel
|
||||||
|
}
|
||||||
|
downloads += newNovelDownloads
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
adapter.setItems(downloads)
|
||||||
|
total.text =
|
||||||
|
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
downloads += newNovelDownloads
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
/**
|
||||||
val type = when (downloadedType.type) {
|
* Load media.json file from the directory and convert it to Media class
|
||||||
DownloadedType.Type.MANGA -> "Manga"
|
* @param downloadedType DownloadedType object
|
||||||
DownloadedType.Type.ANIME -> "Anime"
|
* @return Media object
|
||||||
else -> "Novel"
|
*/
|
||||||
}
|
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
val directory = File(
|
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
|
||||||
)
|
|
||||||
//load media.json and convert to media class with gson
|
|
||||||
return try {
|
return try {
|
||||||
|
val directory = getSubDirectory(
|
||||||
|
context ?: currContext()!!, downloadedType.type,
|
||||||
|
false, downloadedType.titleName
|
||||||
|
)
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
})
|
})
|
||||||
.create()
|
.create()
|
||||||
val media = File(directory, "media.json")
|
val media = directory?.findFile("media.json")
|
||||||
val mediaJson = media.readText()
|
?: return DownloadCompat.loadMediaCompat(downloadedType)
|
||||||
|
val mediaJson =
|
||||||
|
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||||
|
it?.readText()
|
||||||
|
}
|
||||||
gson.fromJson(mediaJson, Media::class.java)
|
gson.fromJson(mediaJson, Media::class.java)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error loading media.json: ${e.message}")
|
Logger.log("Error loading media.json: ${e.message}")
|
||||||
@@ -315,41 +340,38 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||||
val type = when (downloadedType.type) {
|
val type = downloadedType.type.asText()
|
||||||
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
|
//load media.json and convert to media class with gson
|
||||||
try {
|
try {
|
||||||
|
val directory = getSubDirectory(
|
||||||
|
context ?: currContext()!!, downloadedType.type,
|
||||||
|
false, downloadedType.titleName
|
||||||
|
)
|
||||||
val mediaModel = getMedia(downloadedType)!!
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
val cover = File(directory, "cover.jpg")
|
val cover = directory?.findFile("cover.jpg")
|
||||||
val coverUri: Uri? = if (cover.exists()) {
|
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||||
Uri.fromFile(cover)
|
cover.uri
|
||||||
} else null
|
} else null
|
||||||
val banner = File(directory, "banner.jpg")
|
val banner = directory?.findFile("banner.jpg")
|
||||||
val bannerUri: Uri? = if (banner.exists()) {
|
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||||
Uri.fromFile(banner)
|
banner.uri
|
||||||
} else null
|
} else null
|
||||||
|
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
|
||||||
val title = mediaModel.mainName()
|
val title = mediaModel.mainName()
|
||||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||||
val isOngoing =
|
val isOngoing =
|
||||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||||
val isUserScored = mediaModel.userScore != 0
|
val isUserScored = mediaModel.userScore != 0
|
||||||
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
val readChapter = (mediaModel.userProgress ?: "~").toString()
|
||||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||||
val chapters = " Chapters"
|
val chapters = " Chapters"
|
||||||
return OfflineMangaModel(
|
return OfflineMangaModel(
|
||||||
title,
|
title,
|
||||||
score,
|
score,
|
||||||
totalchapter,
|
totalChapter,
|
||||||
readchapter,
|
readChapter,
|
||||||
type,
|
type,
|
||||||
chapters,
|
chapters,
|
||||||
isOngoing,
|
isOngoing,
|
||||||
@@ -358,21 +380,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
bannerUri
|
bannerUri
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error loading media.json: ${e.message}")
|
return try {
|
||||||
Logger.log(e)
|
loadOfflineMangaModelCompat(downloadedType)
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
} catch (e: Exception) {
|
||||||
return OfflineMangaModel(
|
Logger.log("Error loading media.json: ${e.message}")
|
||||||
"unknown",
|
Logger.log(e)
|
||||||
"0",
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
"??",
|
return OfflineMangaModel(
|
||||||
"??",
|
"unknown",
|
||||||
"movie",
|
"0",
|
||||||
"hmm",
|
"??",
|
||||||
isOngoing = false,
|
"??",
|
||||||
isUserScored = false,
|
"movie",
|
||||||
null,
|
"hmm",
|
||||||
null
|
isOngoing = false,
|
||||||
)
|
isUserScored = false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,21 +9,25 @@ import android.content.IntentFilter
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.novel.NovelReadFragment
|
import ani.dantotsu.media.novel.NovelReadFragment
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import com.anggrayudi.storage.file.forceDelete
|
||||||
|
import com.anggrayudi.storage.file.openOutputStream
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
@@ -33,7 +37,6 @@ import eu.kanade.tachiyomi.source.model.SChapterImpl
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -46,8 +49,6 @@ import okio.sink
|
|||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@@ -64,7 +65,7 @@ class NovelDownloaderService : Service() {
|
|||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private var isCurrentlyProcessing = false
|
private var isCurrentlyProcessing = false
|
||||||
|
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
private val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
// This is only required for bound services.
|
// This is only required for bound services.
|
||||||
@@ -247,27 +248,30 @@ class NovelDownloaderService : Service() {
|
|||||||
|
|
||||||
networkHelper.downloadClient.newCall(request).execute().use { response ->
|
networkHelper.downloadClient.newCall(request).execute().use { response ->
|
||||||
// Ensure the response is successful and has a body
|
// Ensure the response is successful and has a body
|
||||||
if (!response.isSuccessful || response.body == null) {
|
if (!response.isSuccessful) {
|
||||||
throw IOException("Failed to download file: ${response.message}")
|
throw IOException("Failed to download file: ${response.message}")
|
||||||
}
|
}
|
||||||
|
val directory = getSubDirectory(
|
||||||
|
this@NovelDownloaderService,
|
||||||
|
MediaType.NOVEL,
|
||||||
|
false,
|
||||||
|
task.title,
|
||||||
|
task.chapter
|
||||||
|
) ?: throw Exception("Directory not found")
|
||||||
|
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
|
||||||
|
|
||||||
val file = File(
|
val file = directory.createFile("application/epub+zip", "0.epub")
|
||||||
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
?: throw Exception("File not created")
|
||||||
"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
|
//download cover
|
||||||
task.coverUrl?.let {
|
task.coverUrl?.let {
|
||||||
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
||||||
}
|
}
|
||||||
|
val outputStream =
|
||||||
|
this@NovelDownloaderService.contentResolver.openOutputStream(file.uri)
|
||||||
|
?: throw Exception("Could not open OutputStream")
|
||||||
|
|
||||||
val sink = file.sink().buffer()
|
val sink = outputStream.sink().buffer()
|
||||||
val responseBody = response.body
|
val responseBody = response.body
|
||||||
val totalBytes = responseBody.contentLength()
|
val totalBytes = responseBody.contentLength()
|
||||||
var downloadedBytes = 0L
|
var downloadedBytes = 0L
|
||||||
@@ -335,7 +339,7 @@ class NovelDownloaderService : Service() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.chapter,
|
task.chapter,
|
||||||
DownloadedType.Type.NOVEL
|
MediaType.NOVEL
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
broadcastDownloadFinished(task.originalLink)
|
broadcastDownloadFinished(task.originalLink)
|
||||||
@@ -352,13 +356,16 @@ class NovelDownloaderService : Service() {
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
private fun saveMediaInfo(task: DownloadTask) {
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
launchIO {
|
launchIO {
|
||||||
val directory = File(
|
val directory =
|
||||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
getSubDirectory(
|
||||||
"Dantotsu/Novel/${task.title}"
|
this@NovelDownloaderService,
|
||||||
)
|
MediaType.NOVEL,
|
||||||
if (!directory.exists()) directory.mkdirs()
|
false,
|
||||||
|
task.title
|
||||||
val file = File(directory, "media.json")
|
) ?: throw Exception("Directory not found")
|
||||||
|
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
|
||||||
|
val file = directory.createFile("application/json", "media.json")
|
||||||
|
?: throw Exception("File not created")
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
@@ -372,33 +379,47 @@ class NovelDownloaderService : Service() {
|
|||||||
|
|
||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
file.writeText(jsonString)
|
try {
|
||||||
|
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
|
||||||
|
if (output == null) throw Exception("Output stream is null")
|
||||||
|
output.write(jsonString.toByteArray())
|
||||||
|
}
|
||||||
|
} catch (e: android.system.ErrnoException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Toast.makeText(
|
||||||
|
this@NovelDownloaderService,
|
||||||
|
"Error while saving: ${e.localizedMessage}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||||
withContext(
|
withContext(
|
||||||
Dispatchers.IO
|
Dispatchers.IO
|
||||||
) {
|
) {
|
||||||
var connection: HttpURLConnection? = null
|
var connection: HttpURLConnection? = null
|
||||||
println("Downloading url $url")
|
Logger.log("Downloading url $url")
|
||||||
try {
|
try {
|
||||||
connection = URL(url).openConnection() as HttpURLConnection
|
connection = URL(url).openConnection() as HttpURLConnection
|
||||||
connection.connect()
|
connection.connect()
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
}
|
}
|
||||||
|
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
|
||||||
val file = File(directory, name)
|
val file =
|
||||||
FileOutputStream(file).use { output ->
|
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||||
|
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
|
||||||
|
if (output == null) throw Exception("Output stream is null")
|
||||||
connection.inputStream.use { input ->
|
connection.inputStream.use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@withContext file.absolutePath
|
return@withContext file.uri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -473,7 +494,6 @@ class NovelDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object NovelServiceDataSingleton {
|
object NovelServiceDataSingleton {
|
||||||
var sourceMedia: Media? = null
|
|
||||||
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package ani.dantotsu.download.video
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import androidx.media3.exoplayer.offline.Download
|
|
||||||
import androidx.media3.exoplayer.offline.DownloadManager
|
|
||||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
|
||||||
import androidx.media3.exoplayer.offline.DownloadService
|
|
||||||
import androidx.media3.exoplayer.scheduler.PlatformScheduler
|
|
||||||
import androidx.media3.exoplayer.scheduler.Scheduler
|
|
||||||
import ani.dantotsu.R
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
|
|
||||||
|
|
||||||
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
|
||||||
|
|
||||||
override fun getForegroundNotification(
|
|
||||||
downloads: MutableList<Download>,
|
|
||||||
notMetRequirements: Int
|
|
||||||
): Notification =
|
|
||||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
|
||||||
this,
|
|
||||||
R.drawable.mono,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
downloads,
|
|
||||||
notMetRequirements
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -7,15 +7,10 @@ import android.app.AlertDialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.media3.common.C
|
|
||||||
import androidx.media3.common.MediaItem
|
|
||||||
import androidx.media3.common.MimeTypes
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.database.StandaloneDatabaseProvider
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
import androidx.media3.datasource.DataSource
|
import androidx.media3.datasource.DataSource
|
||||||
@@ -23,11 +18,8 @@ import androidx.media3.datasource.HttpDataSource
|
|||||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||||
import androidx.media3.datasource.cache.SimpleCache
|
import androidx.media3.datasource.cache.SimpleCache
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
|
||||||
import androidx.media3.exoplayer.offline.Download
|
import androidx.media3.exoplayer.offline.Download
|
||||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
|
||||||
import androidx.media3.exoplayer.offline.DownloadManager
|
import androidx.media3.exoplayer.offline.DownloadManager
|
||||||
import androidx.media3.exoplayer.offline.DownloadService
|
|
||||||
import androidx.media3.exoplayer.scheduler.Requirements
|
import androidx.media3.exoplayer.scheduler.Requirements
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.defaultHeaders
|
import ani.dantotsu.defaultHeaders
|
||||||
@@ -35,93 +27,101 @@ import ani.dantotsu.download.DownloadedType
|
|||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||||
import ani.dantotsu.logError
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.okHttpClient
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
import ani.dantotsu.parsers.SubtitleType
|
|
||||||
import ani.dantotsu.parsers.Video
|
import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.parsers.VideoType
|
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.*
|
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
object Helper {
|
object Helper {
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private var simpleCache: SimpleCache? = null
|
fun startAnimeDownloadService(
|
||||||
|
context: Context,
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
title: String,
|
||||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
episode: String,
|
||||||
val dataSourceFactory = DataSource.Factory {
|
video: Video,
|
||||||
val dataSource: HttpDataSource =
|
subtitle: Subtitle? = null,
|
||||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
sourceMedia: Media? = null,
|
||||||
defaultHeaders.forEach {
|
episodeImage: String? = null
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
) {
|
||||||
}
|
if (!isNotificationPermissionGranted(context)) {
|
||||||
video.file.headers.forEach {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
ActivityCompat.requestPermissions(
|
||||||
}
|
context as Activity,
|
||||||
dataSource
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
}
|
1
|
||||||
val mimeType = when (video.format) {
|
|
||||||
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
|
||||||
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
|
||||||
else -> MimeTypes.APPLICATION_MP4
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
|
||||||
var sub: MediaItem.SubtitleConfiguration? = null
|
|
||||||
if (subtitle != null) {
|
|
||||||
sub = MediaItem.SubtitleConfiguration
|
|
||||||
.Builder(Uri.parse(subtitle.file.url))
|
|
||||||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
|
||||||
.setMimeType(
|
|
||||||
when (subtitle.type) {
|
|
||||||
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
|
||||||
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
|
||||||
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
|
||||||
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.build()
|
}
|
||||||
}
|
}
|
||||||
if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
|
|
||||||
val mediaItem = builder.build()
|
|
||||||
val downloadHelper = DownloadHelper.forMediaItem(
|
|
||||||
context,
|
|
||||||
mediaItem,
|
|
||||||
DefaultRenderersFactory(context),
|
|
||||||
dataSourceFactory
|
|
||||||
)
|
|
||||||
downloadHelper.prepare(object : DownloadHelper.Callback {
|
|
||||||
override fun onPrepared(helper: DownloadHelper) {
|
|
||||||
helper.getDownloadRequest(null).let {
|
|
||||||
DownloadService.sendAddDownload(
|
|
||||||
context,
|
|
||||||
ExoplayerDownloadService::class.java,
|
|
||||||
it,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareError(helper: DownloadHelper, e: IOException) {
|
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
|
||||||
logError(e)
|
title,
|
||||||
|
episode,
|
||||||
|
video,
|
||||||
|
subtitle,
|
||||||
|
sourceMedia,
|
||||||
|
episodeImage
|
||||||
|
)
|
||||||
|
|
||||||
|
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||||
|
val downloadCheck = downloadsManger
|
||||||
|
.queryDownload(title, episode, MediaType.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") { _, _ ->
|
||||||
|
PrefManager.getAnimeDownloadPreferences().edit()
|
||||||
|
.remove(animeDownloadTask.getTaskName())
|
||||||
|
.apply()
|
||||||
|
downloadsManger.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
|
title,
|
||||||
|
episode,
|
||||||
|
MediaType.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
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||||
private var download: DownloadManager? = null
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
return ActivityCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
|
@Deprecated("exoplayer download manager is no longer used")
|
||||||
fun downloadManager(context: Context): DownloadManager {
|
fun downloadManager(context: Context): DownloadManager {
|
||||||
return download ?: let {
|
return download ?: let {
|
||||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||||
@@ -175,96 +175,7 @@ object Helper {
|
|||||||
downloadManager
|
downloadManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Deprecated("exoplayer download manager is no longer used")
|
||||||
private var downloadDirectory: File? = null
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun getDownloadDirectory(context: Context): File {
|
|
||||||
if (downloadDirectory == null) {
|
|
||||||
downloadDirectory = context.getExternalFilesDir(null)
|
|
||||||
if (downloadDirectory == null) {
|
|
||||||
downloadDirectory = context.filesDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
@OptIn(UnstableApi::class)
|
||||||
fun getSimpleCache(context: Context): SimpleCache {
|
fun getSimpleCache(context: Context): SimpleCache {
|
||||||
return if (simpleCache == null) {
|
return if (simpleCache == null) {
|
||||||
@@ -276,14 +187,23 @@ object Helper {
|
|||||||
simpleCache!!
|
simpleCache!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Synchronized
|
||||||
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
@Deprecated("exoplayer download manager is no longer used")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
private fun getDownloadDirectory(context: Context): File {
|
||||||
return ActivityCompat.checkSelfPermission(
|
if (downloadDirectory == null) {
|
||||||
context,
|
downloadDirectory = context.getExternalFilesDir(null)
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
if (downloadDirectory == null) {
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
downloadDirectory = context.filesDir
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return downloadDirectory!!
|
||||||
}
|
}
|
||||||
|
@Deprecated("exoplayer download manager is no longer used")
|
||||||
|
private var download: DownloadManager? = null
|
||||||
|
@Deprecated("exoplayer download manager is no longer used")
|
||||||
|
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||||
|
@Deprecated("exoplayer download manager is no longer used")
|
||||||
|
private var simpleCache: SimpleCache? = null
|
||||||
|
@Deprecated("exoplayer download manager is no longer used")
|
||||||
|
private var downloadDirectory: File? = null
|
||||||
}
|
}
|
||||||
@@ -207,6 +207,21 @@ class AnimeFragment : Fragment() {
|
|||||||
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
|
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
model.getMovies().observe(viewLifecycleOwner) {
|
||||||
|
if (it != null) {
|
||||||
|
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.getTopRated().observe(viewLifecycleOwner) {
|
||||||
|
if (it != null) {
|
||||||
|
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.getMostFav().observe(viewLifecycleOwner) {
|
||||||
|
if (it != null) {
|
||||||
|
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
|
||||||
|
}
|
||||||
|
}
|
||||||
if (animePageAdapter.trendingViewPager != null) {
|
if (animePageAdapter.trendingViewPager != null) {
|
||||||
animePageAdapter.updateHeight()
|
animePageAdapter.updateHeight()
|
||||||
model.getTrending().observe(viewLifecycleOwner) {
|
model.getTrending().observe(viewLifecycleOwner) {
|
||||||
@@ -263,7 +278,7 @@ class AnimeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.loadTrending(1)
|
model.loadTrending(1)
|
||||||
model.loadUpdated()
|
model.loadAll()
|
||||||
model.loadPopular(
|
model.loadPopular(
|
||||||
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||||
PrefName.PopularAnimeList
|
PrefName.PopularAnimeList
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import android.content.Intent
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.LayoutAnimationController
|
import android.view.animation.LayoutAnimationController
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -21,6 +23,7 @@ import ani.dantotsu.R
|
|||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||||
|
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.CalendarActivity
|
import ani.dantotsu.media.CalendarActivity
|
||||||
import ani.dantotsu.media.GenreActivity
|
import ani.dantotsu.media.GenreActivity
|
||||||
@@ -41,6 +44,7 @@ import com.google.android.material.textfield.TextInputLayout
|
|||||||
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
||||||
val ready = MutableLiveData(false)
|
val ready = MutableLiveData(false)
|
||||||
lateinit var binding: ItemAnimePageBinding
|
lateinit var binding: ItemAnimePageBinding
|
||||||
|
private lateinit var trendingBinding: LayoutTrendingBinding
|
||||||
private var trendHandler: Handler? = null
|
private var trendHandler: Handler? = null
|
||||||
private lateinit var trendRun: Runnable
|
private lateinit var trendRun: Runnable
|
||||||
var trendingViewPager: ViewPager2? = null
|
var trendingViewPager: ViewPager2? = null
|
||||||
@@ -53,14 +57,15 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: AnimePageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: AnimePageViewHolder, position: Int) {
|
||||||
binding = holder.binding
|
binding = holder.binding
|
||||||
trendingViewPager = binding.animeTrendingViewPager
|
trendingBinding = LayoutTrendingBinding.bind(binding.root)
|
||||||
|
trendingViewPager = trendingBinding.trendingViewPager
|
||||||
|
|
||||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
|
||||||
val currentColor = textInputLayout.boxBackgroundColor
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
val materialCardView =
|
val materialCardView =
|
||||||
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
|
||||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
@@ -69,16 +74,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||||
|
|
||||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = (-108f).px
|
bottomMargin = (-108f).px
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAvatar()
|
updateAvatar()
|
||||||
|
|
||||||
binding.animeSearchBar.hint = "ANIME"
|
trendingBinding.searchBar.hint = "ANIME"
|
||||||
binding.animeSearchBarText.setOnClickListener {
|
trendingBinding.searchBarText.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
it.context,
|
it.context,
|
||||||
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
|
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
|
||||||
@@ -86,26 +91,28 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSearchBar.setEndIconOnClickListener {
|
trendingBinding.userAvatar.setSafeOnClickListener {
|
||||||
binding.animeSearchBarText.performClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.animeUserAvatar.setSafeOnClickListener {
|
|
||||||
val dialogFragment =
|
val dialogFragment =
|
||||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
|
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
|
||||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
binding.animeUserAvatar.setOnLongClickListener { view ->
|
trendingBinding.userAvatar.setOnLongClickListener { view ->
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
view.context,
|
view.context,
|
||||||
Intent(view.context, ProfileActivity::class.java)
|
Intent(view.context, ProfileActivity::class.java)
|
||||||
.putExtra("userId", Anilist.userid),null
|
.putExtra("userId", Anilist.userid), null
|
||||||
)
|
)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
trendingBinding.searchBar.setEndIconOnClickListener {
|
||||||
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
trendingBinding.searchBar.performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
trendingBinding.notificationCount.visibility =
|
||||||
|
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||||
|
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
binding.animePreviousSeason,
|
binding.animePreviousSeason,
|
||||||
@@ -134,8 +141,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeIncludeList.visibility =
|
binding.animeIncludeList.isVisible = Anilist.userid != null
|
||||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
|
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
|
||||||
|
|
||||||
@@ -159,30 +165,31 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateTrending(adaptor: MediaAdaptor) {
|
fun updateTrending(adaptor: MediaAdaptor) {
|
||||||
binding.animeTrendingProgressBar.visibility = View.GONE
|
trendingBinding.trendingProgressBar.visibility = View.GONE
|
||||||
binding.animeTrendingViewPager.adapter = adaptor
|
trendingBinding.trendingViewPager.adapter = adaptor
|
||||||
binding.animeTrendingViewPager.offscreenPageLimit = 3
|
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
||||||
binding.animeTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
||||||
binding.animeTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
RecyclerView.OVER_SCROLL_NEVER
|
||||||
|
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
binding.animeTrendingViewPager.currentItem =
|
trendingBinding.trendingViewPager.currentItem += 1
|
||||||
binding.animeTrendingViewPager.currentItem + 1
|
|
||||||
}
|
}
|
||||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
super.onPageSelected(position)
|
super.onPageSelected(position)
|
||||||
trendHandler!!.removeCallbacks(trendRun)
|
trendHandler?.removeCallbacks(trendRun)
|
||||||
trendHandler!!.postDelayed(trendRun, 4000)
|
if (PrefManager.getVal(PrefName.TrendingScroller)) {
|
||||||
|
trendHandler!!.postDelayed(trendRun, 4000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.animeTrendingViewPager.layoutAnimation =
|
trendingBinding.trendingViewPager.layoutAnimation =
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.animeTitleContainer.startAnimation(setSlideUp())
|
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
||||||
binding.animeListContainer.layoutAnimation =
|
binding.animeListContainer.layoutAnimation =
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.animeSeasonsCont.layoutAnimation =
|
binding.animeSeasonsCont.layoutAnimation =
|
||||||
@@ -190,36 +197,83 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateRecent(adaptor: MediaAdaptor) {
|
fun updateRecent(adaptor: MediaAdaptor) {
|
||||||
binding.animeUpdatedProgressBar.visibility = View.GONE
|
binding.apply {
|
||||||
binding.animeUpdatedRecyclerView.adapter = adaptor
|
init(
|
||||||
binding.animeUpdatedRecyclerView.layoutManager =
|
adaptor,
|
||||||
|
animeUpdatedRecyclerView,
|
||||||
|
animeUpdatedProgressBar,
|
||||||
|
animeRecently
|
||||||
|
)
|
||||||
|
animePopular.visibility = View.VISIBLE
|
||||||
|
animePopular.startAnimation(setSlideUp())
|
||||||
|
if (adaptor.itemCount == 0) {
|
||||||
|
animeRecentlyContainer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMovies(adaptor: MediaAdaptor) {
|
||||||
|
binding.apply {
|
||||||
|
init(
|
||||||
|
adaptor,
|
||||||
|
animeMoviesRecyclerView,
|
||||||
|
animeMoviesProgressBar,
|
||||||
|
animeMovies
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||||
|
binding.apply {
|
||||||
|
init(
|
||||||
|
adaptor,
|
||||||
|
animeTopRatedRecyclerView,
|
||||||
|
animeTopRatedProgressBar,
|
||||||
|
animeTopRated
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||||
|
binding.apply {
|
||||||
|
init(
|
||||||
|
adaptor,
|
||||||
|
animeMostFavRecyclerView,
|
||||||
|
animeMostFavProgressBar,
|
||||||
|
animeMostFav
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
|
||||||
|
progress.visibility = View.GONE
|
||||||
|
recyclerView.adapter = adaptor
|
||||||
|
recyclerView.layoutManager =
|
||||||
LinearLayoutManager(
|
LinearLayoutManager(
|
||||||
binding.animeUpdatedRecyclerView.context,
|
recyclerView.context,
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
recyclerView.visibility = View.VISIBLE
|
||||||
|
title.visibility = View.VISIBLE
|
||||||
binding.animeRecently.visibility = View.VISIBLE
|
title.startAnimation(setSlideUp())
|
||||||
binding.animeRecently.startAnimation(setSlideUp())
|
recyclerView.layoutAnimation =
|
||||||
binding.animeUpdatedRecyclerView.layoutAnimation =
|
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.animePopular.visibility = View.VISIBLE
|
|
||||||
binding.animePopular.startAnimation(setSlideUp())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAvatar() {
|
fun updateAvatar() {
|
||||||
if (Anilist.avatar != null && ready.value == true) {
|
if (Anilist.avatar != null && ready.value == true) {
|
||||||
binding.animeUserAvatar.loadImage(Anilist.avatar)
|
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||||
binding.animeUserAvatar.imageTintList = null
|
trendingBinding.userAvatar.imageTintList = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNotificationCount() {
|
fun updateNotificationCount() {
|
||||||
if (this::binding.isInitialized) {
|
if (this::binding.isInitialized) {
|
||||||
binding.animeNotificationCount.visibility =
|
trendingBinding.notificationCount.visibility =
|
||||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||||
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import android.content.Intent
|
|||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.LayoutAnimationController
|
import android.view.animation.LayoutAnimationController
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@@ -78,10 +80,13 @@ class HomeFragment : Fragment() {
|
|||||||
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
||||||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
||||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
|
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||||
blurImage(binding.homeUserBg, Anilist.bg)
|
blurImage(
|
||||||
|
if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen,
|
||||||
|
Anilist.bg
|
||||||
|
)
|
||||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||||
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||||
|
|
||||||
binding.homeAnimeList.setOnClickListener {
|
binding.homeAnimeList.setOnClickListener {
|
||||||
@@ -123,9 +128,10 @@ class HomeFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
binding.homeUserAvatarContainer.setOnLongClickListener {
|
binding.homeUserAvatarContainer.setOnLongClickListener {
|
||||||
|
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
|
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
|
||||||
.putExtra("userId", Anilist.userid),null
|
.putExtra("userId", Anilist.userid), null
|
||||||
)
|
)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -134,6 +140,7 @@ class HomeFragment : Fragment() {
|
|||||||
bottomMargin = navBarHeight
|
bottomMargin = navBarHeight
|
||||||
}
|
}
|
||||||
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
|
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
|
||||||
|
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
var reached = false
|
var reached = false
|
||||||
@@ -372,10 +379,11 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
|
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
|
||||||
if (_binding != null) {
|
if (_binding != null) {
|
||||||
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||||
}
|
}
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|||||||
@@ -160,11 +160,37 @@ class MangaFragment : Fragment() {
|
|||||||
})
|
})
|
||||||
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
|
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
|
||||||
if (i == true) {
|
if (i == true) {
|
||||||
model.getTrendingNovel().observe(viewLifecycleOwner) {
|
model.getPopularNovel().observe(viewLifecycleOwner) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
|
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
model.getPopularManga().observe(viewLifecycleOwner) {
|
||||||
|
if (it != null) {
|
||||||
|
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.getPopularManhwa().observe(viewLifecycleOwner) {
|
||||||
|
if (it != null) {
|
||||||
|
mangaPageAdapter.updateTrendingManhwa(
|
||||||
|
MediaAdaptor(
|
||||||
|
0,
|
||||||
|
it,
|
||||||
|
requireActivity()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.getTopRated().observe(viewLifecycleOwner) {
|
||||||
|
if (it != null) {
|
||||||
|
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.getMostFav().observe(viewLifecycleOwner) {
|
||||||
|
if (it != null) {
|
||||||
|
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
|
||||||
|
}
|
||||||
|
}
|
||||||
if (mangaPageAdapter.trendingViewPager != null) {
|
if (mangaPageAdapter.trendingViewPager != null) {
|
||||||
mangaPageAdapter.updateHeight()
|
mangaPageAdapter.updateHeight()
|
||||||
model.getTrending().observe(viewLifecycleOwner) {
|
model.getTrending().observe(viewLifecycleOwner) {
|
||||||
@@ -237,7 +263,7 @@ class MangaFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.loadTrending()
|
model.loadTrending()
|
||||||
model.loadTrendingNovel()
|
model.loadAll()
|
||||||
model.loadPopular(
|
model.loadPopular(
|
||||||
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||||
PrefName.PopularMangaList
|
PrefName.PopularMangaList
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import android.content.Intent
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.LayoutAnimationController
|
import android.view.animation.LayoutAnimationController
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -21,6 +23,7 @@ import ani.dantotsu.R
|
|||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||||
|
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.GenreActivity
|
import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
@@ -40,6 +43,7 @@ import com.google.android.material.textfield.TextInputLayout
|
|||||||
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
||||||
val ready = MutableLiveData(false)
|
val ready = MutableLiveData(false)
|
||||||
lateinit var binding: ItemMangaPageBinding
|
lateinit var binding: ItemMangaPageBinding
|
||||||
|
private lateinit var trendingBinding: LayoutTrendingBinding
|
||||||
private var trendHandler: Handler? = null
|
private var trendHandler: Handler? = null
|
||||||
private lateinit var trendRun: Runnable
|
private lateinit var trendRun: Runnable
|
||||||
var trendingViewPager: ViewPager2? = null
|
var trendingViewPager: ViewPager2? = null
|
||||||
@@ -52,33 +56,34 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: MangaPageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MangaPageViewHolder, position: Int) {
|
||||||
binding = holder.binding
|
binding = holder.binding
|
||||||
trendingViewPager = binding.mangaTrendingViewPager
|
trendingBinding = LayoutTrendingBinding.bind(binding.root)
|
||||||
|
trendingViewPager = trendingBinding.trendingViewPager
|
||||||
|
|
||||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
|
||||||
val currentColor = textInputLayout.boxBackgroundColor
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
val materialCardView =
|
val materialCardView =
|
||||||
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
|
||||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
val color = typedValue.data
|
val color = typedValue.data
|
||||||
|
|
||||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||||
|
|
||||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = (-108f).px
|
bottomMargin = (-108f).px
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAvatar()
|
updateAvatar()
|
||||||
binding.mangaNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||||
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||||
binding.mangaSearchBar.hint = "MANGA"
|
trendingBinding.searchBar.hint = "MANGA"
|
||||||
binding.mangaSearchBarText.setOnClickListener {
|
trendingBinding.searchBarText.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
it.context,
|
it.context,
|
||||||
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
|
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
|
||||||
@@ -86,22 +91,23 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
trendingBinding.userAvatar.setSafeOnClickListener {
|
||||||
val dialogFragment =
|
val dialogFragment =
|
||||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
|
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
|
||||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
binding.mangaUserAvatar.setOnLongClickListener { view ->
|
trendingBinding.userAvatar.setOnLongClickListener { view ->
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
view.context,
|
view.context,
|
||||||
Intent(view.context, ProfileActivity::class.java)
|
Intent(view.context, ProfileActivity::class.java)
|
||||||
.putExtra("userId", Anilist.userid),null
|
.putExtra("userId", Anilist.userid), null
|
||||||
)
|
)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaSearchBar.setEndIconOnClickListener {
|
trendingBinding.searchBar.setEndIconOnClickListener {
|
||||||
binding.mangaSearchBarText.performClick()
|
trendingBinding.searchBarText.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaGenreImage.loadImage("https://s4.anilist.co/file/anilistcdn/media/manga/banner/105778-wk5qQ7zAaTGl.jpg")
|
binding.mangaGenreImage.loadImage("https://s4.anilist.co/file/anilistcdn/media/manga/banner/105778-wk5qQ7zAaTGl.jpg")
|
||||||
@@ -125,8 +131,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaIncludeList.visibility =
|
binding.mangaIncludeList.isVisible = Anilist.userid != null
|
||||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
|
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
|
||||||
|
|
||||||
@@ -148,63 +153,121 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateTrending(adaptor: MediaAdaptor) {
|
fun updateTrending(adaptor: MediaAdaptor) {
|
||||||
binding.mangaTrendingProgressBar.visibility = View.GONE
|
trendingBinding.trendingProgressBar.visibility = View.GONE
|
||||||
binding.mangaTrendingViewPager.adapter = adaptor
|
trendingBinding.trendingViewPager.adapter = adaptor
|
||||||
binding.mangaTrendingViewPager.offscreenPageLimit = 3
|
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
||||||
binding.mangaTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
||||||
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
RecyclerView.OVER_SCROLL_NEVER
|
||||||
|
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
binding.mangaTrendingViewPager.currentItem += 1
|
trendingBinding.trendingViewPager.currentItem += 1
|
||||||
}
|
}
|
||||||
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
super.onPageSelected(position)
|
super.onPageSelected(position)
|
||||||
trendHandler!!.removeCallbacks(trendRun)
|
trendHandler?.removeCallbacks(trendRun)
|
||||||
trendHandler!!.postDelayed(trendRun, 4000)
|
if (PrefManager.getVal(PrefName.TrendingScroller))
|
||||||
|
trendHandler!!.postDelayed(trendRun, 4000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.mangaTrendingViewPager.layoutAnimation =
|
trendingBinding.trendingViewPager.layoutAnimation =
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.mangaTitleContainer.startAnimation(setSlideUp())
|
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
||||||
binding.mangaListContainer.layoutAnimation =
|
binding.mangaListContainer.layoutAnimation =
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTrendingManga(adaptor: MediaAdaptor) {
|
||||||
|
binding.apply {
|
||||||
|
init(
|
||||||
|
adaptor,
|
||||||
|
mangaTrendingMangaRecyclerView,
|
||||||
|
mangaTrendingMangaProgressBar,
|
||||||
|
mangaTrendingManga
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
|
||||||
|
binding.apply {
|
||||||
|
init(
|
||||||
|
adaptor,
|
||||||
|
mangaTrendingManhwaRecyclerView,
|
||||||
|
mangaTrendingManhwaProgressBar,
|
||||||
|
mangaTrendingManhwa
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNovel(adaptor: MediaAdaptor) {
|
fun updateNovel(adaptor: MediaAdaptor) {
|
||||||
binding.mangaNovelProgressBar.visibility = View.GONE
|
binding.apply {
|
||||||
binding.mangaNovelRecyclerView.adapter = adaptor
|
init(
|
||||||
binding.mangaNovelRecyclerView.layoutManager =
|
adaptor,
|
||||||
|
mangaNovelRecyclerView,
|
||||||
|
mangaNovelProgressBar,
|
||||||
|
mangaNovel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||||
|
binding.apply {
|
||||||
|
init(
|
||||||
|
adaptor,
|
||||||
|
mangaTopRatedRecyclerView,
|
||||||
|
mangaTopRatedProgressBar,
|
||||||
|
mangaTopRated
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||||
|
binding.apply {
|
||||||
|
init(
|
||||||
|
adaptor,
|
||||||
|
mangaMostFavRecyclerView,
|
||||||
|
mangaMostFavProgressBar,
|
||||||
|
mangaMostFav
|
||||||
|
)
|
||||||
|
mangaPopular.visibility = View.VISIBLE
|
||||||
|
mangaPopular.startAnimation(setSlideUp())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
|
||||||
|
progress.visibility = View.GONE
|
||||||
|
recyclerView.adapter = adaptor
|
||||||
|
recyclerView.layoutManager =
|
||||||
LinearLayoutManager(
|
LinearLayoutManager(
|
||||||
binding.mangaNovelRecyclerView.context,
|
recyclerView.context,
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
recyclerView.visibility = View.VISIBLE
|
||||||
|
title.visibility = View.VISIBLE
|
||||||
binding.mangaNovel.visibility = View.VISIBLE
|
title.startAnimation(setSlideUp())
|
||||||
binding.mangaNovel.startAnimation(setSlideUp())
|
recyclerView.layoutAnimation =
|
||||||
binding.mangaNovelRecyclerView.layoutAnimation =
|
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.mangaPopular.visibility = View.VISIBLE
|
|
||||||
binding.mangaPopular.startAnimation(setSlideUp())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAvatar() {
|
fun updateAvatar() {
|
||||||
if (Anilist.avatar != null && ready.value == true) {
|
if (Anilist.avatar != null && ready.value == true) {
|
||||||
binding.mangaUserAvatar.loadImage(Anilist.avatar)
|
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||||
binding.mangaUserAvatar.imageTintList = null
|
trendingBinding.userAvatar.imageTintList = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNotificationCount() {
|
fun updateNotificationCount() {
|
||||||
if (this::binding.isInitialized) {
|
if (this::binding.isInitialized) {
|
||||||
binding.mangaNotificationCount.visibility =
|
trendingBinding.notificationCount.visibility =
|
||||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||||
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package ani.dantotsu.media
|
|||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
data class Author(
|
data class Author(
|
||||||
val id: Int,
|
var id: Int,
|
||||||
val name: String?,
|
var name: String?,
|
||||||
val image: String?,
|
var image: String?,
|
||||||
val role: String?,
|
var role: String?,
|
||||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
|
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
|
||||||
|
var character: ArrayList<Character>? = null
|
||||||
) : Serializable
|
) : Serializable
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import ani.dantotsu.EmptyAdapter
|
import ani.dantotsu.EmptyAdapter
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
@@ -32,7 +33,6 @@ class AuthorActivity : AppCompatActivity() {
|
|||||||
private val model: OtherDetailsViewModel by viewModels()
|
private val model: OtherDetailsViewModel by viewModels()
|
||||||
private var author: Author? = null
|
private var author: Author? = null
|
||||||
private var loaded = false
|
private var loaded = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -55,14 +55,15 @@ class AuthorActivity : AppCompatActivity() {
|
|||||||
binding.studioClose.setOnClickListener {
|
binding.studioClose.setOnClickListener {
|
||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
model.getAuthor().observe(this) {
|
model.getAuthor().observe(this) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
author = it
|
author = it
|
||||||
loaded = true
|
loaded = true
|
||||||
binding.studioProgressBar.visibility = View.GONE
|
binding.studioProgressBar.visibility = View.GONE
|
||||||
binding.studioRecycler.visibility = View.VISIBLE
|
binding.studioRecycler.visibility = View.VISIBLE
|
||||||
|
if (author!!.yearMedia.isNullOrEmpty()) {
|
||||||
|
binding.studioRecycler.visibility = View.GONE
|
||||||
|
}
|
||||||
val titlePosition = arrayListOf<Int>()
|
val titlePosition = arrayListOf<Int>()
|
||||||
val concatAdapter = ConcatAdapter()
|
val concatAdapter = ConcatAdapter()
|
||||||
val map = author!!.yearMedia ?: return@observe
|
val map = author!!.yearMedia ?: return@observe
|
||||||
@@ -89,9 +90,19 @@ class AuthorActivity : AppCompatActivity() {
|
|||||||
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
|
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
|
||||||
concatAdapter.addAdapter(EmptyAdapter(empty))
|
concatAdapter.addAdapter(EmptyAdapter(empty))
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.studioRecycler.adapter = concatAdapter
|
binding.studioRecycler.adapter = concatAdapter
|
||||||
binding.studioRecycler.layoutManager = gridLayoutManager
|
binding.studioRecycler.layoutManager = gridLayoutManager
|
||||||
|
|
||||||
|
binding.charactersRecycler.visibility = View.VISIBLE
|
||||||
|
binding.charactersText.visibility = View.VISIBLE
|
||||||
|
binding.charactersRecycler.adapter =
|
||||||
|
CharacterAdapter(author!!.character ?: arrayListOf())
|
||||||
|
binding.charactersRecycler.layoutManager =
|
||||||
|
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
if (author!!.character.isNullOrEmpty()) {
|
||||||
|
binding.charactersRecycler.visibility = View.GONE
|
||||||
|
binding.charactersText.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -16,7 +15,7 @@ import ani.dantotsu.setAnimation
|
|||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
class AuthorAdapter(
|
class AuthorAdapter(
|
||||||
private val authorList: ArrayList<Author>
|
private val authorList: ArrayList<Author>,
|
||||||
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
|
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
|
||||||
val binding =
|
val binding =
|
||||||
@@ -24,8 +23,7 @@ class AuthorAdapter(
|
|||||||
return AuthorViewHolder(binding)
|
return AuthorViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
|
||||||
override fun onBindViewHolder(holder:AuthorViewHolder, position: Int) {
|
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(binding.root.context, holder.binding.root)
|
setAnimation(binding.root.context, holder.binding.root)
|
||||||
val author = authorList[position]
|
val author = authorList[position]
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -16,8 +14,8 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityListBinding
|
import ani.dantotsu.databinding.ActivityListBinding
|
||||||
|
import ani.dantotsu.hideSystemBarsExtendView
|
||||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||||
import ani.dantotsu.navBarHeight
|
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
@@ -34,7 +32,6 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
private var selectedTabIdx = 1
|
private var selectedTabIdx = 1
|
||||||
private val model: OtherDetailsViewModel by viewModels()
|
private val model: OtherDetailsViewModel by viewModels()
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -45,13 +42,6 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||||
val primaryColor = typedValue.data
|
val primaryColor = typedValue.data
|
||||||
val typedValue2 = TypedValue()
|
|
||||||
theme.resolveAttribute(
|
|
||||||
com.google.android.material.R.attr.colorOnBackground,
|
|
||||||
typedValue2,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
val titleTextColor = typedValue2.data
|
|
||||||
val typedValue3 = TypedValue()
|
val typedValue3 = TypedValue()
|
||||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||||
val primaryTextColor = typedValue3.data
|
val primaryTextColor = typedValue3.data
|
||||||
@@ -74,10 +64,7 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
binding.root.fitsSystemWindows = false
|
binding.root.fitsSystemWindows = false
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
window.setFlags(
|
hideSystemBarsExtendView()
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
||||||
)
|
|
||||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
topMargin = statusBarHeight
|
topMargin = statusBarHeight
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ data class Character(
|
|||||||
var age: String? = null,
|
var age: String? = null,
|
||||||
var gender: String? = null,
|
var gender: String? = null,
|
||||||
var dateOfBirth: FuzzyDate? = null,
|
var dateOfBirth: FuzzyDate? = null,
|
||||||
var roles: ArrayList<Media>? = null
|
var roles: ArrayList<Media>? = null,
|
||||||
|
val voiceActor: ArrayList<Author>? = null,
|
||||||
) : Serializable
|
) : Serializable
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -24,12 +23,13 @@ class CharacterAdapter(
|
|||||||
return CharacterViewHolder(binding)
|
return CharacterViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(binding.root.context, holder.binding.root)
|
setAnimation(binding.root.context, holder.binding.root)
|
||||||
val character = characterList[position]
|
val character = characterList[position]
|
||||||
binding.itemCompactRelation.text = character.role + " "
|
val whitespace = "${character.role} "
|
||||||
|
character.voiceActor
|
||||||
|
binding.itemCompactRelation.text = whitespace
|
||||||
binding.itemCompactImage.loadImage(character.image)
|
binding.itemCompactImage.loadImage(character.image)
|
||||||
binding.itemCompactTitle.text = character.name
|
binding.itemCompactTitle.text = character.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.activity.viewModels
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.math.MathUtils.clamp
|
import androidx.core.math.MathUtils.clamp
|
||||||
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -94,7 +95,8 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
}
|
}
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
character.isFav = Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
|
character.isFav =
|
||||||
|
Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.characterFav.setImageResource(
|
binding.characterFav.setImageResource(
|
||||||
@@ -152,7 +154,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
|
binding.characterProgress.isGone = loaded
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.currActivity
|
import ani.dantotsu.currActivity
|
||||||
@@ -20,23 +21,36 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
|||||||
return GenreViewHolder(binding)
|
return GenreViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val desc =
|
val desc =
|
||||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
|
||||||
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
(if (character.dateOfBirth.toString() != "")
|
||||||
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
|
||||||
"Male" -> currActivity()!!.getString(R.string.male)
|
(if (character.gender != "null")
|
||||||
"Female" -> currActivity()!!.getString(R.string.female)
|
currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||||
else -> character.gender
|
currActivity()!!.getString(R.string.male) -> currActivity()!!.getString(
|
||||||
} else "") + "\n" + character.description
|
R.string.male
|
||||||
|
)
|
||||||
|
|
||||||
|
currActivity()!!.getString(R.string.female) -> currActivity()!!.getString(
|
||||||
|
R.string.female
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> character.gender
|
||||||
|
} else "") + "\n" + character.description
|
||||||
|
|
||||||
binding.characterDesc.isTextSelectable
|
binding.characterDesc.isTextSelectable
|
||||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||||
.usePlugin(SpoilerPlugin()).build()
|
.usePlugin(SpoilerPlugin()).build()
|
||||||
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
|
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
|
||||||
|
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
|
||||||
|
binding.voiceActorRecycler.layoutManager = LinearLayoutManager(
|
||||||
|
activity, LinearLayoutManager.HORIZONTAL, false
|
||||||
|
)
|
||||||
|
if (binding.voiceActorRecycler.adapter!!.itemCount == 0) {
|
||||||
|
binding.voiceActorContainer.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
override fun getItemCount(): Int = 1
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ani.dantotsu.connections.anilist.api.MediaList
|
|||||||
import ani.dantotsu.connections.anilist.api.MediaType
|
import ani.dantotsu.connections.anilist.api.MediaType
|
||||||
import ani.dantotsu.media.anime.Anime
|
import ani.dantotsu.media.anime.Anime
|
||||||
import ani.dantotsu.media.manga.Manga
|
import ani.dantotsu.media.manga.Manga
|
||||||
|
import ani.dantotsu.profile.User
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
|
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ data class Media(
|
|||||||
var cover: String? = null,
|
var cover: String? = null,
|
||||||
var banner: String? = null,
|
var banner: String? = null,
|
||||||
var relation: String? = null,
|
var relation: String? = null,
|
||||||
var popularity: Int? = null,
|
var favourites: Int? = null,
|
||||||
|
|
||||||
var isAdult: Boolean,
|
var isAdult: Boolean,
|
||||||
var isFav: Boolean = false,
|
var isFav: Boolean = false,
|
||||||
@@ -56,6 +57,9 @@ data class Media(
|
|||||||
var trailer: String? = null,
|
var trailer: String? = null,
|
||||||
var startDate: FuzzyDate? = null,
|
var startDate: FuzzyDate? = null,
|
||||||
var endDate: FuzzyDate? = null,
|
var endDate: FuzzyDate? = null,
|
||||||
|
var popularity: Int? = null,
|
||||||
|
|
||||||
|
var timeUntilAiring: Long? = null,
|
||||||
|
|
||||||
var characters: ArrayList<Character>? = null,
|
var characters: ArrayList<Character>? = null,
|
||||||
var staff: ArrayList<Author>? = null,
|
var staff: ArrayList<Author>? = null,
|
||||||
@@ -63,7 +67,7 @@ data class Media(
|
|||||||
var sequel: Media? = null,
|
var sequel: Media? = null,
|
||||||
var relations: ArrayList<Media>? = null,
|
var relations: ArrayList<Media>? = null,
|
||||||
var recommendations: ArrayList<Media>? = null,
|
var recommendations: ArrayList<Media>? = null,
|
||||||
|
var users: ArrayList<User>? = null,
|
||||||
var vrvId: String? = null,
|
var vrvId: String? = null,
|
||||||
var crunchySlug: String? = null,
|
var crunchySlug: String? = null,
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ data class Media(
|
|||||||
name = apiMedia.title!!.english,
|
name = apiMedia.title!!.english,
|
||||||
nameRomaji = apiMedia.title!!.romaji,
|
nameRomaji = apiMedia.title!!.romaji,
|
||||||
userPreferredName = apiMedia.title!!.userPreferred,
|
userPreferredName = apiMedia.title!!.userPreferred,
|
||||||
cover = apiMedia.coverImage?.large,
|
cover = apiMedia.coverImage?.large ?: apiMedia.coverImage?.medium,
|
||||||
banner = apiMedia.bannerImage,
|
banner = apiMedia.bannerImage,
|
||||||
status = apiMedia.status.toString(),
|
status = apiMedia.status.toString(),
|
||||||
isFav = apiMedia.isFavourite!!,
|
isFav = apiMedia.isFavourite!!,
|
||||||
@@ -95,6 +99,8 @@ data class Media(
|
|||||||
meanScore = apiMedia.meanScore,
|
meanScore = apiMedia.meanScore,
|
||||||
startDate = apiMedia.startDate,
|
startDate = apiMedia.startDate,
|
||||||
endDate = apiMedia.endDate,
|
endDate = apiMedia.endDate,
|
||||||
|
favourites = apiMedia.favourites,
|
||||||
|
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000 },
|
||||||
anime = if (apiMedia.type == MediaType.ANIME) Anime(
|
anime = if (apiMedia.type == MediaType.ANIME) Anime(
|
||||||
totalEpisodes = apiMedia.episodes,
|
totalEpisodes = apiMedia.episodes,
|
||||||
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
|
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
|
||||||
@@ -109,7 +115,8 @@ data class Media(
|
|||||||
this.userScore = mediaList.score?.toInt() ?: 0
|
this.userScore = mediaList.score?.toInt() ?: 0
|
||||||
this.userStatus = mediaList.status?.toString()
|
this.userStatus = mediaList.status?.toString()
|
||||||
this.userUpdatedAt = mediaList.updatedAt?.toLong()
|
this.userUpdatedAt = mediaList.updatedAt?.toLong()
|
||||||
this.genres = mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
|
this.genres =
|
||||||
|
mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
|
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
@@ -15,25 +13,25 @@ import android.widget.ImageView
|
|||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.util.Pair
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.blurImage
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.databinding.ItemMediaCompactBinding
|
import ani.dantotsu.databinding.ItemMediaCompactBinding
|
||||||
import ani.dantotsu.databinding.ItemMediaLargeBinding
|
import ani.dantotsu.databinding.ItemMediaLargeBinding
|
||||||
import ani.dantotsu.databinding.ItemMediaPageBinding
|
import ani.dantotsu.databinding.ItemMediaPageBinding
|
||||||
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
|
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
|
||||||
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.setAnimation
|
||||||
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
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
|
|
||||||
import com.bumptech.glide.request.RequestOptions
|
|
||||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
|
|
||||||
@@ -85,7 +83,7 @@ class MediaAdaptor(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (type) {
|
when (type) {
|
||||||
0 -> {
|
0 -> {
|
||||||
@@ -94,8 +92,8 @@ class MediaAdaptor(
|
|||||||
val media = mediaList?.getOrNull(position)
|
val media = mediaList?.getOrNull(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
b.itemCompactOngoing.visibility =
|
b.itemCompactOngoing.isVisible =
|
||||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore
|
((if (media.userScore == 0) (media.meanScore
|
||||||
@@ -140,8 +138,8 @@ class MediaAdaptor(
|
|||||||
if (media != null) {
|
if (media != null) {
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||||
b.itemCompactOngoing.visibility =
|
b.itemCompactOngoing.isVisible =
|
||||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore
|
((if (media.userScore == 0) (media.meanScore
|
||||||
@@ -151,25 +149,30 @@ class MediaAdaptor(
|
|||||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||||
)
|
)
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
val itemTotal = " " + if ((media.anime.totalEpisodes
|
||||||
?: 0) != 1
|
?: 0) != 1
|
||||||
) currActivity()!!.getString(R.string.episode_plural)
|
) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(
|
||||||
else currActivity()!!.getString(R.string.episode_singular)
|
R.string.episode_singular
|
||||||
|
)
|
||||||
|
b.itemTotal.text = itemTotal
|
||||||
b.itemCompactTotal.text =
|
b.itemCompactTotal.text =
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||||
?: "??").toString()) else (media.anime.totalEpisodes
|
?: "??").toString()) else (media.anime.totalEpisodes
|
||||||
?: "??").toString()
|
?: "??").toString()
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
val itemTotal = " " + if ((media.manga.totalChapters
|
||||||
?: 0) != 1
|
?: 0) != 1
|
||||||
) currActivity()!!.getString(R.string.chapter_plural)
|
) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(
|
||||||
else currActivity()!!.getString(R.string.chapter_singular)
|
R.string.chapter_singular
|
||||||
|
)
|
||||||
|
b.itemTotal.text = itemTotal
|
||||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||||
}
|
}
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
|
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
|
||||||
|
val start = mediaList.size
|
||||||
mediaList.addAll(mediaList)
|
mediaList.addAll(mediaList)
|
||||||
notifyDataSetChanged()
|
val end = mediaList.size - start
|
||||||
|
notifyItemRangeInserted(start, end)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +181,7 @@ class MediaAdaptor(
|
|||||||
val b = (holder as MediaPageViewHolder).binding
|
val b = (holder as MediaPageViewHolder).binding
|
||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
|
|
||||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
if (bannerAnimations)
|
if (bannerAnimations)
|
||||||
@@ -187,9 +191,12 @@ class MediaAdaptor(
|
|||||||
AccelerateDecelerateInterpolator()
|
AccelerateDecelerateInterpolator()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
blurImage(
|
||||||
b.itemCompactOngoing.visibility =
|
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
|
||||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
media.banner ?: media.cover
|
||||||
|
)
|
||||||
|
b.itemCompactOngoing.isVisible =
|
||||||
|
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore
|
((if (media.userScore == 0) (media.meanScore
|
||||||
@@ -236,9 +243,12 @@ class MediaAdaptor(
|
|||||||
AccelerateDecelerateInterpolator()
|
AccelerateDecelerateInterpolator()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
blurImage(
|
||||||
b.itemCompactOngoing.visibility =
|
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
|
||||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
media.banner ?: media.cover
|
||||||
|
)
|
||||||
|
b.itemCompactOngoing.isVisible =
|
||||||
|
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore
|
((if (media.userScore == 0) (media.meanScore
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package ani.dantotsu.media
|
|||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Rect
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
@@ -12,18 +11,19 @@ import android.view.GestureDetector
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.text.bold
|
import androidx.core.text.bold
|
||||||
import androidx.core.text.color
|
import androidx.core.text.color
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginBottom
|
import androidx.core.view.marginBottom
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.core.view.updateMargins
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -54,6 +54,7 @@ import ani.dantotsu.settings.saving.PrefName
|
|||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import ani.dantotsu.util.LauncherWrapper
|
||||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -62,20 +63,21 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|
||||||
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||||
|
lateinit var launcher: LauncherWrapper
|
||||||
lateinit var binding: ActivityMediaBinding
|
lateinit var binding: ActivityMediaBinding
|
||||||
private val scope = lifecycleScope
|
private val scope = lifecycleScope
|
||||||
private val model: MediaDetailsViewModel by viewModels()
|
private val model: MediaDetailsViewModel by viewModels()
|
||||||
lateinit var tabLayout: TripleNavAdapter
|
|
||||||
var selected = 0
|
var selected = 0
|
||||||
|
lateinit var navBar: AnimatedBottomBar
|
||||||
var anime = true
|
var anime = true
|
||||||
private var adult = false
|
private var adult = false
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||||
@@ -83,8 +85,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
media =
|
media = Anilist.query.getMedia(id, false) ?: emptyMedia()
|
||||||
Anilist.query.getMedia(id, false) ?: emptyMedia()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +94,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val contract = ActivityResultContracts.OpenDocumentTree()
|
||||||
|
launcher = LauncherWrapper(this, contract)
|
||||||
|
|
||||||
mediaSingleton = null
|
mediaSingleton = null
|
||||||
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||||
MediaSingleton.bitmap = null
|
MediaSingleton.bitmap = null
|
||||||
@@ -100,26 +104,38 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||||
|
navBar = binding.mediaBottomBar
|
||||||
|
|
||||||
val isVertical = resources.configuration.orientation
|
// Ui init
|
||||||
//Ui init
|
|
||||||
|
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
bottomMargin = navBarHeight
|
||||||
|
}
|
||||||
val oldMargin = binding.mediaViewPager.marginBottom
|
val oldMargin = binding.mediaViewPager.marginBottom
|
||||||
AndroidBug5497Workaround.assistActivity(this) {
|
AndroidBug5497Workaround.assistActivity(this) {
|
||||||
if (it) {
|
if (it) {
|
||||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = 0
|
bottomMargin = 0
|
||||||
}
|
}
|
||||||
binding.mediaTabContainer.visibility = View.GONE
|
navBar.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = oldMargin
|
bottomMargin = oldMargin
|
||||||
}
|
}
|
||||||
binding.mediaTabContainer.visibility = View.VISIBLE
|
navBar.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val navBarRightMargin = if (resources.configuration.orientation ==
|
||||||
|
Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
) navBarHeight else 0
|
||||||
|
val navBarBottomMargin = if (resources.configuration.orientation ==
|
||||||
|
Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
) 0 else navBarHeight
|
||||||
|
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
rightMargin = navBarRightMargin
|
||||||
|
bottomMargin = navBarBottomMargin
|
||||||
|
}
|
||||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||||
@@ -147,7 +163,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
val banner =
|
val banner =
|
||||||
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||||
val viewPager = binding.mediaViewPager
|
val viewPager = binding.mediaViewPager
|
||||||
//tabLayout = binding.mediaTab as AnimatedBottomBar
|
|
||||||
viewPager.isUserInputEnabled = false
|
viewPager.isUserInputEnabled = false
|
||||||
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||||
|
|
||||||
@@ -157,9 +172,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
binding.mediaCoverImage.loadImage(media.cover)
|
binding.mediaCoverImage.loadImage(media.cover)
|
||||||
binding.mediaCoverImage.setOnLongClickListener {
|
binding.mediaCoverImage.setOnLongClickListener {
|
||||||
|
val coverTitle = "${media.userPreferredName}[Cover]"
|
||||||
ImageViewDialog.newInstance(
|
ImageViewDialog.newInstance(
|
||||||
this,
|
this,
|
||||||
media.userPreferredName + "[Cover]",
|
coverTitle,
|
||||||
media.cover
|
media.cover
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -176,9 +192,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongClick(event: MotionEvent) {
|
override fun onLongClick(event: MotionEvent) {
|
||||||
|
val bannerTitle = "${media.userPreferredName}[Banner]"
|
||||||
ImageViewDialog.newInstance(
|
ImageViewDialog.newInstance(
|
||||||
this@MediaDetailsActivity,
|
this@MediaDetailsActivity,
|
||||||
media.userPreferredName + "[Banner]",
|
bannerTitle,
|
||||||
media.banner ?: media.cover
|
media.banner ?: media.cover
|
||||||
)
|
)
|
||||||
banner.performClick()
|
banner.performClick()
|
||||||
@@ -186,7 +203,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
})
|
})
|
||||||
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
||||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||||
binding.mediaTitle.text = " ${media.userPreferredName}"
|
val mediaTitle = " ${media.userPreferredName}"
|
||||||
|
binding.mediaTitle.text = mediaTitle
|
||||||
binding.incognito.visibility = View.VISIBLE
|
binding.incognito.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
binding.mediaTitle.text = media.userPreferredName
|
binding.mediaTitle.text = media.userPreferredName
|
||||||
@@ -210,20 +228,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
R.drawable.ic_round_favorite_24
|
R.drawable.ic_round_favorite_24
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val typedValue = TypedValue()
|
|
||||||
this.theme.resolveAttribute(
|
|
||||||
com.google.android.material.R.attr.colorSecondary,
|
|
||||||
typedValue,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
val color = typedValue.data
|
|
||||||
val typedValue2 = TypedValue()
|
|
||||||
this.theme.resolveAttribute(
|
|
||||||
com.google.android.material.R.attr.colorSecondary,
|
|
||||||
typedValue2,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
val color2 = typedValue.data
|
|
||||||
|
|
||||||
PopImageButton(
|
PopImageButton(
|
||||||
scope,
|
scope,
|
||||||
@@ -231,7 +235,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
R.drawable.ic_round_favorite_24,
|
R.drawable.ic_round_favorite_24,
|
||||||
R.drawable.ic_round_favorite_border_24,
|
R.drawable.ic_round_favorite_border_24,
|
||||||
R.color.bg_opp,
|
R.color.bg_opp,
|
||||||
R.color.violet_400,//TODO: Change to colorSecondary
|
R.color.violet_400,
|
||||||
media.isFav
|
media.isFav
|
||||||
) {
|
) {
|
||||||
media.isFav = it
|
media.isFav = it
|
||||||
@@ -246,13 +250,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
@SuppressLint("ResourceType")
|
@SuppressLint("ResourceType")
|
||||||
fun total() {
|
fun total() {
|
||||||
val text = SpannableStringBuilder().apply {
|
val text = SpannableStringBuilder().apply {
|
||||||
val typedValue = TypedValue()
|
val mediaTypedValue = TypedValue()
|
||||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||||
com.google.android.material.R.attr.colorOnBackground,
|
com.google.android.material.R.attr.colorOnBackground,
|
||||||
typedValue,
|
mediaTypedValue,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
val white = typedValue.data
|
val white = mediaTypedValue.data
|
||||||
if (media.userStatus != null) {
|
if (media.userStatus != null) {
|
||||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
@@ -342,18 +346,16 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
progress()
|
progress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tabLayout = TripleNavAdapter(
|
|
||||||
binding.mediaTab1,
|
|
||||||
binding.mediaTab2,
|
|
||||||
binding.mediaTab3,
|
|
||||||
media.anime != null,
|
|
||||||
media.format ?: "",
|
|
||||||
isVertical == 1
|
|
||||||
)
|
|
||||||
adult = media.isAdult
|
adult = media.isAdult
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
viewPager.adapter =
|
viewPager.adapter =
|
||||||
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME, media, intent.getIntExtra("commentId", -1))
|
ViewPagerAdapter(
|
||||||
|
supportFragmentManager,
|
||||||
|
lifecycle,
|
||||||
|
SupportedMedia.ANIME,
|
||||||
|
media,
|
||||||
|
intent.getIntExtra("commentId", -1)
|
||||||
|
)
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
viewPager.adapter = ViewPagerAdapter(
|
viewPager.adapter = ViewPagerAdapter(
|
||||||
supportFragmentManager,
|
supportFragmentManager,
|
||||||
@@ -365,31 +367,47 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
anime = false
|
anime = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
selected = media.selected!!.window
|
selected = media.selected!!.window
|
||||||
binding.mediaTitle.translationX = -screenWidth
|
binding.mediaTitle.translationX = -screenWidth
|
||||||
|
|
||||||
tabLayout.selectionListener = { selected, newId ->
|
val infoTab = navBar.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
|
||||||
binding.commentInputLayout.visibility = if (selected == 2) View.VISIBLE else View.GONE
|
val watchTab = if (anime) {
|
||||||
this.selected = selected
|
navBar.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
|
||||||
selectFromID(newId)
|
} else if (media.format == "NOVEL") {
|
||||||
viewPager.setCurrentItem(selected, false)
|
navBar.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
|
||||||
val sel = model.loadSelected(media, isDownload)
|
} else {
|
||||||
sel.window = selected
|
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
|
||||||
model.saveSelected(media.id, sel)
|
|
||||||
}
|
}
|
||||||
tabLayout.selectTab(selected)
|
val commentTab =
|
||||||
selectFromID(tabLayout.selected)
|
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
|
||||||
viewPager.setCurrentItem(selected, false)
|
navBar.addTab(infoTab)
|
||||||
|
navBar.addTab(watchTab)
|
||||||
|
navBar.addTab(commentTab)
|
||||||
if (model.continueMedia == null && media.cameFromContinue) {
|
if (model.continueMedia == null && media.cameFromContinue) {
|
||||||
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
||||||
selected = 1
|
selected = 1
|
||||||
}
|
}
|
||||||
val frag = intent.getStringExtra("FRAGMENT_TO_LOAD")
|
if (intent.getStringExtra("FRAGMENT_TO_LOAD") != null) selected = 2
|
||||||
if (frag != null) {
|
if (viewPager.currentItem != selected) viewPager.post {
|
||||||
selected = 2
|
viewPager.setCurrentItem(selected, false)
|
||||||
}
|
}
|
||||||
|
binding.commentInputLayout.isVisible = selected == 2
|
||||||
|
navBar.selectTabAt(selected)
|
||||||
|
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
||||||
|
override fun onTabSelected(
|
||||||
|
lastIndex: Int,
|
||||||
|
lastTab: AnimatedBottomBar.Tab?,
|
||||||
|
newIndex: Int,
|
||||||
|
newTab: AnimatedBottomBar.Tab
|
||||||
|
) {
|
||||||
|
selected = newIndex
|
||||||
|
binding.commentInputLayout.isVisible = selected == 2
|
||||||
|
viewPager.setCurrentItem(selected, true)
|
||||||
|
val sel = model.loadSelected(media, isDownload)
|
||||||
|
sel.window = selected
|
||||||
|
model.saveSelected(media.id, sel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
||||||
live.observe(this) {
|
live.observe(this) {
|
||||||
@@ -402,40 +420,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectFromID(id: Int) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
when (id) {
|
super.onConfigurationChanged(newConfig)
|
||||||
R.id.info -> {
|
val rightMargin = if (resources.configuration.orientation ==
|
||||||
selected = 0
|
Configuration.ORIENTATION_LANDSCAPE
|
||||||
}
|
) navBarHeight else 0
|
||||||
|
val bottomMargin = if (resources.configuration.orientation ==
|
||||||
R.id.watch, R.id.read -> {
|
Configuration.ORIENTATION_LANDSCAPE
|
||||||
selected = 1
|
) 0 else navBarHeight
|
||||||
}
|
val params: ViewGroup.MarginLayoutParams =
|
||||||
|
navBar.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
R.id.comment -> {
|
params.updateMargins(right = rightMargin, bottom = bottomMargin)
|
||||||
selected = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun idFromSelect(): Int {
|
|
||||||
if (anime) when (selected) {
|
|
||||||
0 -> return R.id.info
|
|
||||||
1 -> return R.id.watch
|
|
||||||
2 -> return R.id.comment
|
|
||||||
}
|
|
||||||
else when (selected) {
|
|
||||||
0 -> return R.id.info
|
|
||||||
1 -> return R.id.read
|
|
||||||
2 -> return R.id.comment
|
|
||||||
}
|
|
||||||
return R.id.info
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (this::tabLayout.isInitialized) {
|
navBar.selectTabAt(selected)
|
||||||
tabLayout.selectTab(selected)
|
|
||||||
}
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +442,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
ANIME, MANGA, NOVEL
|
ANIME, MANGA, NOVEL
|
||||||
}
|
}
|
||||||
|
|
||||||
//ViewPager
|
// ViewPager
|
||||||
private class ViewPagerAdapter(
|
private class ViewPagerAdapter(
|
||||||
fragmentManager: FragmentManager,
|
fragmentManager: FragmentManager,
|
||||||
lifecycle: Lifecycle,
|
lifecycle: Lifecycle,
|
||||||
@@ -462,6 +461,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
SupportedMedia.MANGA -> MangaReadFragment()
|
SupportedMedia.MANGA -> MangaReadFragment()
|
||||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> {
|
2 -> {
|
||||||
val fragment = CommentsFragment()
|
val fragment = CommentsFragment()
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
@@ -489,13 +489,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
binding.mediaCover.visibility =
|
binding.mediaCover.visibility =
|
||||||
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||||
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).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,
|
|
||||||
typedValue,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
val color = typedValue.data
|
|
||||||
if (percentage >= percent && !isCollapsed) {
|
if (percentage >= percent && !isCollapsed) {
|
||||||
isCollapsed = true
|
isCollapsed = true
|
||||||
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
|
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.anime.Episode
|
import ani.dantotsu.media.anime.Episode
|
||||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||||
import ani.dantotsu.media.manga.MangaChapter
|
import ani.dantotsu.media.manga.MangaChapter
|
||||||
@@ -29,6 +28,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
|||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
@@ -52,26 +52,23 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
if (isDownload) {
|
if (isDownload) {
|
||||||
data.sourceIndex = if (media.anime != null) {
|
data.sourceIndex = when {
|
||||||
AnimeSources.list.size - 1
|
media.anime != null -> {
|
||||||
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
AnimeSources.list.size - 1
|
||||||
MangaSources.list.size - 1
|
}
|
||||||
} else {
|
|
||||||
NovelSources.list.size - 1
|
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
|
||||||
|
MangaSources.list.size - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
NovelSources.list.size - 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadSelectedStringLocation(sourceName: String): Int {
|
|
||||||
//find the location of the source in the list
|
|
||||||
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
|
||||||
if (location == -1) {
|
|
||||||
location = 0
|
|
||||||
}
|
|
||||||
return location
|
|
||||||
}
|
|
||||||
|
|
||||||
var continueMedia: Boolean? = null
|
var continueMedia: Boolean? = null
|
||||||
private var loading = false
|
private var loading = false
|
||||||
|
|
||||||
@@ -152,10 +149,10 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
watchSources?.get(i)?.apply {
|
watchSources?.get(i)?.apply {
|
||||||
if (!post && !allowsPreloading) return@apply
|
if (!post && !allowsPreloading) return@apply
|
||||||
ep.sEpisode?.let {
|
ep.sEpisode?.let {
|
||||||
loadByVideoServers(link, ep.extra, it) {
|
loadByVideoServers(link, ep.extra, it) { extractor ->
|
||||||
if (it.videos.isNotEmpty()) {
|
if (extractor.videos.isNotEmpty()) {
|
||||||
list.add(it)
|
list.add(extractor)
|
||||||
ep.extractorCallback?.invoke(it)
|
ep.extractorCallback?.invoke(extractor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,7 +288,6 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
suspend fun loadMangaChapterImages(
|
suspend fun loadMangaChapterImages(
|
||||||
chapter: MangaChapter,
|
chapter: MangaChapter,
|
||||||
selected: Selected,
|
selected: Selected,
|
||||||
series: String,
|
|
||||||
post: Boolean = true
|
post: Boolean = true
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package ani.dantotsu.media
|
|||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.CountDownTimer
|
import android.os.CountDownTimer
|
||||||
@@ -16,16 +15,33 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||||
import ani.dantotsu.databinding.*
|
import ani.dantotsu.copyToClipboard
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
|
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||||
|
import ani.dantotsu.databinding.FragmentMediaInfoBinding
|
||||||
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
|
import ani.dantotsu.databinding.ItemQuelsBinding
|
||||||
|
import ani.dantotsu.databinding.ItemTitleChipgroupBinding
|
||||||
|
import ani.dantotsu.databinding.ItemTitleRecyclerBinding
|
||||||
|
import ani.dantotsu.databinding.ItemTitleSearchBinding
|
||||||
|
import ani.dantotsu.databinding.ItemTitleTextBinding
|
||||||
|
import ani.dantotsu.databinding.ItemTitleTrailerBinding
|
||||||
|
import ani.dantotsu.displayTimer
|
||||||
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.px
|
||||||
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
@@ -37,7 +53,6 @@ import java.io.Serializable
|
|||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
class MediaInfoFragment : Fragment() {
|
class MediaInfoFragment : Fragment() {
|
||||||
private var _binding: FragmentMediaInfoBinding? = null
|
private var _binding: FragmentMediaInfoBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
@@ -46,6 +61,8 @@ class MediaInfoFragment : Fragment() {
|
|||||||
private var type = "ANIME"
|
private var type = "ANIME"
|
||||||
private val genreModel: GenresViewModel by activityViewModels()
|
private val genreModel: GenresViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val tripleTab = "\t\t\t"
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -63,8 +80,8 @@ class MediaInfoFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val model: MediaDetailsViewModel by activityViewModels()
|
val model: MediaDetailsViewModel by activityViewModels()
|
||||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
binding.mediaInfoProgressBar.isGone = loaded
|
||||||
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
binding.mediaInfoContainer.isVisible = loaded
|
||||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||||
|
|
||||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||||
@@ -75,17 +92,18 @@ class MediaInfoFragment : Fragment() {
|
|||||||
if (media != null && !loaded) {
|
if (media != null && !loaded) {
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
||||||
|
|
||||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||||
binding.mediaInfoContainer.visibility = View.VISIBLE
|
binding.mediaInfoContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
|
val infoName = tripleTab + (media.name ?: media.nameRomaji)
|
||||||
|
binding.mediaInfoName.text = infoName
|
||||||
binding.mediaInfoName.setOnLongClickListener {
|
binding.mediaInfoName.setOnLongClickListener {
|
||||||
copyToClipboard(media.name ?: media.nameRomaji)
|
copyToClipboard(media.name ?: media.nameRomaji)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
|
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
val infoNameRomanji = tripleTab + media.nameRomaji
|
||||||
|
binding.mediaInfoNameRomaji.text = infoNameRomanji
|
||||||
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
||||||
copyToClipboard(media.nameRomaji)
|
copyToClipboard(media.nameRomaji)
|
||||||
true
|
true
|
||||||
@@ -97,6 +115,8 @@ class MediaInfoFragment : Fragment() {
|
|||||||
binding.mediaInfoSource.text = media.source
|
binding.mediaInfoSource.text = media.source
|
||||||
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
||||||
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
||||||
|
binding.mediaInfoPopularity.text = media.popularity.toString()
|
||||||
|
binding.mediaInfoFavorites.text = media.favourites.toString()
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
val episodeDuration = media.anime.episodeDuration
|
val episodeDuration = media.anime.episodeDuration
|
||||||
|
|
||||||
@@ -125,8 +145,10 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoSeason.text =
|
val seasonInfo =
|
||||||
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
|
"${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
|
||||||
|
binding.mediaInfoSeason.text = seasonInfo
|
||||||
|
|
||||||
if (media.anime.mainStudio != null) {
|
if (media.anime.mainStudio != null) {
|
||||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||||
@@ -160,9 +182,12 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||||
binding.mediaInfoTotal.text =
|
val infoTotal = if (media.anime.nextAiringEpisode != null)
|
||||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
|
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
|
||||||
?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString()
|
else
|
||||||
|
(media.anime.totalEpisodes ?: "~").toString()
|
||||||
|
binding.mediaInfoTotal.text = infoTotal
|
||||||
|
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
type = "MANGA"
|
type = "MANGA"
|
||||||
binding.mediaInfoTotalTitle.setText(R.string.total_chaps)
|
binding.mediaInfoTotalTitle.setText(R.string.total_chaps)
|
||||||
@@ -189,8 +214,10 @@ class MediaInfoFragment : Fragment() {
|
|||||||
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
|
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||||
)
|
)
|
||||||
binding.mediaInfoDescription.text =
|
val infoDesc =
|
||||||
"\t\t\t" + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
|
tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
|
||||||
|
binding.mediaInfoDescription.text = infoDesc
|
||||||
|
|
||||||
binding.mediaInfoDescription.setOnClickListener {
|
binding.mediaInfoDescription.setOnClickListener {
|
||||||
if (binding.mediaInfoDescription.maxLines == 5) {
|
if (binding.mediaInfoDescription.maxLines == 5) {
|
||||||
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100)
|
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100)
|
||||||
@@ -200,8 +227,7 @@ class MediaInfoFragment : Fragment() {
|
|||||||
.setDuration(400).start()
|
.setDuration(400).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
displayTimer(media, binding.mediaInfoContainer)
|
||||||
countDown(media, binding.mediaInfoContainer)
|
|
||||||
val parent = _binding?.mediaInfoContainer!!
|
val parent = _binding?.mediaInfoContainer!!
|
||||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||||
|
|
||||||
@@ -413,113 +439,155 @@ class MediaInfoFragment : Fragment() {
|
|||||||
|
|
||||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||||
if (media.sequel != null || media.prequel != null) {
|
if (media.sequel != null || media.prequel != null) {
|
||||||
val bind = ItemQuelsBinding.inflate(
|
ItemQuelsBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
)
|
).apply {
|
||||||
|
|
||||||
if (media.sequel != null) {
|
if (media.sequel != null) {
|
||||||
bind.mediaInfoSequel.visibility = View.VISIBLE
|
mediaInfoSequel.visibility = View.VISIBLE
|
||||||
bind.mediaInfoSequelImage.loadImage(
|
mediaInfoSequelImage.loadImage(
|
||||||
media.sequel!!.banner ?: media.sequel!!.cover
|
media.sequel!!.banner ?: media.sequel!!.cover
|
||||||
)
|
|
||||||
bind.mediaInfoSequel.setSafeOnClickListener {
|
|
||||||
ContextCompat.startActivity(
|
|
||||||
requireContext(),
|
|
||||||
Intent(
|
|
||||||
requireContext(),
|
|
||||||
MediaDetailsActivity::class.java
|
|
||||||
).putExtra(
|
|
||||||
"media",
|
|
||||||
media.sequel as Serializable
|
|
||||||
), null
|
|
||||||
)
|
)
|
||||||
}
|
mediaInfoSequel.setSafeOnClickListener {
|
||||||
}
|
ContextCompat.startActivity(
|
||||||
if (media.prequel != null) {
|
|
||||||
bind.mediaInfoPrequel.visibility = View.VISIBLE
|
|
||||||
bind.mediaInfoPrequelImage.loadImage(
|
|
||||||
media.prequel!!.banner ?: media.prequel!!.cover
|
|
||||||
)
|
|
||||||
bind.mediaInfoPrequel.setSafeOnClickListener {
|
|
||||||
ContextCompat.startActivity(
|
|
||||||
requireContext(),
|
|
||||||
Intent(
|
|
||||||
requireContext(),
|
requireContext(),
|
||||||
MediaDetailsActivity::class.java
|
Intent(
|
||||||
).putExtra(
|
requireContext(),
|
||||||
"media",
|
MediaDetailsActivity::class.java
|
||||||
media.prequel as Serializable
|
).putExtra(
|
||||||
), null
|
"media",
|
||||||
)
|
media.sequel as Serializable
|
||||||
|
), null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (media.prequel != null) {
|
||||||
|
mediaInfoPrequel.visibility = View.VISIBLE
|
||||||
|
mediaInfoPrequelImage.loadImage(
|
||||||
|
media.prequel!!.banner ?: media.prequel!!.cover
|
||||||
|
)
|
||||||
|
mediaInfoPrequel.setSafeOnClickListener {
|
||||||
|
ContextCompat.startActivity(
|
||||||
|
requireContext(),
|
||||||
|
Intent(
|
||||||
|
requireContext(),
|
||||||
|
MediaDetailsActivity::class.java
|
||||||
|
).putExtra(
|
||||||
|
"media",
|
||||||
|
media.prequel as Serializable
|
||||||
|
), null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent.addView(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemTitleSearchBinding.inflate(
|
||||||
|
LayoutInflater.from(context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
).apply {
|
||||||
|
|
||||||
|
titleSearchImage.loadImage(media.banner ?: media.cover)
|
||||||
|
titleSearchText.text =
|
||||||
|
getString(R.string.search_title, media.mainName())
|
||||||
|
titleSearchCard.setSafeOnClickListener {
|
||||||
|
val query = Intent(requireContext(), SearchActivity::class.java)
|
||||||
|
.putExtra("type", "ANIME")
|
||||||
|
.putExtra("query", media.mainName())
|
||||||
|
.putExtra("search", true)
|
||||||
|
ContextCompat.startActivity(requireContext(), query, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.addView(root)
|
||||||
}
|
}
|
||||||
parent.addView(bind.root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val bindi = ItemTitleRecyclerBinding.inflate(
|
ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
)
|
).apply {
|
||||||
|
|
||||||
bindi.itemRecycler.adapter =
|
itemRecycler.adapter =
|
||||||
MediaAdaptor(0, media.relations!!, requireActivity())
|
MediaAdaptor(0, media.relations!!, requireActivity())
|
||||||
bindi.itemRecycler.layoutManager = LinearLayoutManager(
|
itemRecycler.layoutManager = LinearLayoutManager(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
parent.addView(bindi.root)
|
parent.addView(root)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||||
val bind = ItemTitleRecyclerBinding.inflate(
|
ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
)
|
).apply {
|
||||||
bind.itemTitle.setText(R.string.characters)
|
itemTitle.setText(R.string.characters)
|
||||||
bind.itemRecycler.adapter =
|
itemRecycler.adapter =
|
||||||
CharacterAdapter(media.characters!!)
|
CharacterAdapter(media.characters!!)
|
||||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
itemRecycler.layoutManager = LinearLayoutManager(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
parent.addView(bind.root)
|
parent.addView(root)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!media.staff.isNullOrEmpty() && !offline) {
|
if (!media.staff.isNullOrEmpty() && !offline) {
|
||||||
val bind = ItemTitleRecyclerBinding.inflate(
|
ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
)
|
).apply {
|
||||||
bind.itemTitle.setText(R.string.staff)
|
itemTitle.setText(R.string.staff)
|
||||||
bind.itemRecycler.adapter =
|
itemRecycler.adapter =
|
||||||
AuthorAdapter(media.staff!!)
|
AuthorAdapter(media.staff!!)
|
||||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
itemRecycler.layoutManager = LinearLayoutManager(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
parent.addView(bind.root)
|
parent.addView(root)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
||||||
val bind = ItemTitleRecyclerBinding.inflate(
|
ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
)
|
).apply {
|
||||||
bind.itemTitle.setText(R.string.recommended)
|
itemTitle.setText(R.string.recommended)
|
||||||
bind.itemRecycler.adapter =
|
itemRecycler.adapter =
|
||||||
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
||||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
itemRecycler.layoutManager = LinearLayoutManager(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
parent.addView(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!media.users.isNullOrEmpty() && !offline) {
|
||||||
|
ItemTitleRecyclerBinding.inflate(
|
||||||
|
LayoutInflater.from(context),
|
||||||
|
parent,
|
||||||
false
|
false
|
||||||
)
|
).apply {
|
||||||
parent.addView(bind.root)
|
itemTitle.setText(R.string.social)
|
||||||
|
itemRecycler.adapter =
|
||||||
|
MediaSocialAdapter(media.users!!)
|
||||||
|
itemRecycler.layoutManager = LinearLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
LinearLayoutManager.HORIZONTAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
parent.addView(root)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,11 +612,12 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onViewCreated(view, null)
|
super.onViewCreated(view, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
binding.mediaInfoProgressBar.isGone = loaded
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputFilter.LengthFilter
|
import android.text.InputFilter.LengthFilter
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
@@ -11,11 +10,18 @@ import android.widget.ArrayAdapter
|
|||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
|
import ani.dantotsu.DatePickerFragment
|
||||||
|
import ani.dantotsu.InputFilterMinMax
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.tryWith
|
||||||
import com.google.android.material.materialswitch.MaterialSwitch
|
import com.google.android.material.materialswitch.MaterialSwitch
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -36,7 +42,6 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||||
var media: Media?
|
var media: Media?
|
||||||
@@ -168,9 +173,10 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
val init =
|
val init =
|
||||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||||
.toInt() else 0
|
.toInt() else 0
|
||||||
if (init < (total
|
if (init < (total ?: 5000)) {
|
||||||
?: 5000)
|
val progressText = "${init + 1}"
|
||||||
) binding.mediaListProgress.setText((init + 1).toString())
|
binding.mediaListProgress.setText(progressText)
|
||||||
|
}
|
||||||
if (init + 1 == (total ?: 5000)) {
|
if (init + 1 == (total ?: 5000)) {
|
||||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||||
onComplete()
|
onComplete()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputFilter.LengthFilter
|
import android.text.InputFilter.LengthFilter
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
@@ -10,11 +9,16 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
|
import ani.dantotsu.InputFilterMinMax
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.snackString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -54,7 +58,6 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||||
val scope = viewLifecycleOwner.lifecycleScope
|
val scope = viewLifecycleOwner.lifecycleScope
|
||||||
@@ -68,7 +71,7 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
MAL.query.deleteList(media.anime != null, media.idMAL)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
snackString("Failed to delete because of... ${e.message}")
|
snackString(getString(R.string.delete_fail_reason, e.message))
|
||||||
}
|
}
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
@@ -154,7 +157,10 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||||||
val init =
|
val init =
|
||||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||||
.toInt() else 0
|
.toInt() else 0
|
||||||
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
|
if (init < (total ?: 5000)) {
|
||||||
|
val progressText = "${init + 1}"
|
||||||
|
binding.mediaListProgress.setText(progressText)
|
||||||
|
}
|
||||||
if (init + 1 == (total ?: 5000)) {
|
if (init + 1 == (total ?: 5000)) {
|
||||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||||
}
|
}
|
||||||
|
|||||||
146
app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
Normal file
146
app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.regex.Matcher
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
object MediaNameAdapter {
|
||||||
|
|
||||||
|
private const val REGEX_ITEM = "[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
|
||||||
|
private const val REGEX_PART_NUMBER = "(?<!part\\s)\\b(\\d+)\\b"
|
||||||
|
private const val REGEX_EPISODE =
|
||||||
|
"(episode|episodio|ep|e)${REGEX_ITEM}\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||||
|
private const val REGEX_SEASON = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||||
|
private const val REGEX_SUBDUB = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
||||||
|
private const val REGEX_CHAPTER = "(chapter|chap|ch|c)${REGEX_ITEM}"
|
||||||
|
|
||||||
|
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
||||||
|
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, 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(REGEX_SUBDUB, 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(REGEX_SEASON, Pattern.CASE_INSENSITIVE)
|
||||||
|
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||||
|
|
||||||
|
return if (seasonMatcher.find()) {
|
||||||
|
seasonMatcher.group(2)?.toInt()
|
||||||
|
} else {
|
||||||
|
text.toIntOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findEpisodeNumber(text: String): Float? {
|
||||||
|
val episodePattern: Pattern = Pattern.compile(REGEX_EPISODE, 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(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
|
||||||
|
val failedEpisodeNumberMatcher: Matcher =
|
||||||
|
failedEpisodeNumberPattern.matcher(text)
|
||||||
|
if (failedEpisodeNumberMatcher.find()) {
|
||||||
|
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text.toFloatOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeEpisodeNumber(text: String): String {
|
||||||
|
val regexPattern = Regex(REGEX_EPISODE, 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(REGEX_EPISODE, RegexOption.IGNORE_CASE)
|
||||||
|
val removedNumber = text.replace(regexPattern, "")
|
||||||
|
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||||
|
val failedEpisodeNumberPattern =
|
||||||
|
Regex(REGEX_PART_NUMBER, RegexOption.IGNORE_CASE)
|
||||||
|
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||||
|
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removedNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findChapterNumber(text: String): Float? {
|
||||||
|
val pattern: Pattern = Pattern.compile(REGEX_CHAPTER, Pattern.CASE_INSENSITIVE)
|
||||||
|
val matcher: Matcher = pattern.matcher(text)
|
||||||
|
|
||||||
|
return if (matcher.find()) {
|
||||||
|
matcher.group(2)?.toFloat()
|
||||||
|
} else {
|
||||||
|
val failedChapterNumberPattern: Pattern =
|
||||||
|
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
|
||||||
|
val failedChapterNumberMatcher: Matcher =
|
||||||
|
failedChapterNumberPattern.matcher(text)
|
||||||
|
if (failedChapterNumberMatcher.find()) {
|
||||||
|
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||||
|
} else {
|
||||||
|
text.toFloatOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt
Normal file
70
app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.databinding.ItemFollowerGridBinding
|
||||||
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.profile.ProfileActivity
|
||||||
|
import ani.dantotsu.profile.User
|
||||||
|
import ani.dantotsu.setAnimation
|
||||||
|
|
||||||
|
class MediaSocialAdapter(private val user: ArrayList<User>) :
|
||||||
|
RecyclerView.Adapter<MediaSocialAdapter.DeveloperViewHolder>() {
|
||||||
|
|
||||||
|
inner class DeveloperViewHolder(val binding: ItemFollowerGridBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeveloperViewHolder {
|
||||||
|
return DeveloperViewHolder(
|
||||||
|
ItemFollowerGridBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onBindViewHolder(holder: DeveloperViewHolder, position: Int) {
|
||||||
|
holder.binding.apply {
|
||||||
|
val user = user[position]
|
||||||
|
val score = user.score?.div(10.0) ?: 0.0
|
||||||
|
setAnimation(root.context, root)
|
||||||
|
profileUserName.text = user.name
|
||||||
|
profileInfo.apply {
|
||||||
|
text = when (user.status) {
|
||||||
|
"CURRENT" -> "WATCHING"
|
||||||
|
else -> user.status ?: ""
|
||||||
|
}
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
profileCompactUserProgress.text = user.progress.toString()
|
||||||
|
profileCompactScore.text = score.toString()
|
||||||
|
profileCompactTotal.text = " | ${user.totalEpisodes ?: "~"}"
|
||||||
|
profileUserAvatar.loadImage(user.pfp)
|
||||||
|
|
||||||
|
val scoreDrawable = if (score == 0.0) R.drawable.score else R.drawable.user_score
|
||||||
|
profileCompactScoreBG.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
background = ContextCompat.getDrawable(root.context, scoreDrawable)
|
||||||
|
}
|
||||||
|
|
||||||
|
profileCompactProgressContainer.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
profileUserAvatar.setOnClickListener {
|
||||||
|
val intent = Intent(root.context, ProfileActivity::class.java).apply {
|
||||||
|
putExtra("userId", user.id)
|
||||||
|
}
|
||||||
|
ContextCompat.startActivity(root.context, intent, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = user.size
|
||||||
|
}
|
||||||
56
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
56
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
interface Type {
|
||||||
|
fun asText(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class MediaType : Type {
|
||||||
|
ANIME,
|
||||||
|
MANGA,
|
||||||
|
NOVEL;
|
||||||
|
|
||||||
|
override fun asText(): String {
|
||||||
|
return when (this) {
|
||||||
|
ANIME -> "Anime"
|
||||||
|
MANGA -> "Manga"
|
||||||
|
NOVEL -> "Novel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromText(string: String): MediaType? {
|
||||||
|
return when (string) {
|
||||||
|
"Anime" -> ANIME
|
||||||
|
"Manga" -> MANGA
|
||||||
|
"Novel" -> NOVEL
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class AddonType : Type {
|
||||||
|
TORRENT,
|
||||||
|
DOWNLOAD;
|
||||||
|
|
||||||
|
override fun asText(): String {
|
||||||
|
return when (this) {
|
||||||
|
TORRENT -> "Torrent"
|
||||||
|
DOWNLOAD -> "Download"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromText(string: String): AddonType? {
|
||||||
|
return when (string) {
|
||||||
|
"Torrent" -> TORRENT
|
||||||
|
"Download" -> DOWNLOAD
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ class OtherDetailsViewModel : ViewModel() {
|
|||||||
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
||||||
suspend fun loadCalendar() {
|
suspend fun loadCalendar() {
|
||||||
val curr = System.currentTimeMillis() / 1000
|
val curr = System.currentTimeMillis() / 1000
|
||||||
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
|
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
|
||||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||||
val map = mutableMapOf<String, MutableList<Media>>()
|
val map = mutableMapOf<String, MutableList<Media>>()
|
||||||
val idMap = mutableMapOf<String, MutableList<Int>>()
|
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
|||||||
return ProgressViewHolder(binding)
|
return ProgressViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) {
|
||||||
val progressBar = holder.binding.root
|
val progressBar = holder.binding.root
|
||||||
bar = progressBar
|
bar = progressBar
|
||||||
|
|||||||
@@ -4,24 +4,30 @@ import android.annotation.SuppressLint
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePaddingRelative
|
import androidx.core.view.updatePaddingRelative
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||||
import ani.dantotsu.connections.anilist.SearchResults
|
import ani.dantotsu.connections.anilist.SearchResults
|
||||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||||
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.px
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
|
|
||||||
class SearchActivity : AppCompatActivity() {
|
class SearchActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivitySearchBinding
|
private lateinit var binding: ActivitySearchBinding
|
||||||
@@ -64,11 +70,18 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
intent.getStringExtra("type") ?: "ANIME",
|
intent.getStringExtra("type") ?: "ANIME",
|
||||||
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
|
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
|
||||||
onList = listOnly,
|
onList = listOnly,
|
||||||
|
search = intent.getStringExtra("query"),
|
||||||
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
|
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
|
||||||
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
|
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
|
||||||
sort = intent.getStringExtra("sortBy"),
|
sort = intent.getStringExtra("sortBy"),
|
||||||
|
status = intent.getStringExtra("status"),
|
||||||
|
source = intent.getStringExtra("source"),
|
||||||
|
countryOfOrigin = intent.getStringExtra("country"),
|
||||||
season = intent.getStringExtra("season"),
|
season = intent.getStringExtra("season"),
|
||||||
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
|
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")
|
||||||
|
?.toIntOrNull() else null,
|
||||||
|
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")
|
||||||
|
?.toIntOrNull() else null,
|
||||||
results = mutableListOf(),
|
results = mutableListOf(),
|
||||||
hasNextPage = false
|
hasNextPage = false
|
||||||
)
|
)
|
||||||
@@ -127,8 +140,12 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
excludedTags = it.excludedTags
|
excludedTags = it.excludedTags
|
||||||
tags = it.tags
|
tags = it.tags
|
||||||
season = it.season
|
season = it.season
|
||||||
|
startYear = it.startYear
|
||||||
seasonYear = it.seasonYear
|
seasonYear = it.seasonYear
|
||||||
|
status = it.status
|
||||||
|
source = it.source
|
||||||
format = it.format
|
format = it.format
|
||||||
|
countryOfOrigin = it.countryOfOrigin
|
||||||
page = it.page
|
page = it.page
|
||||||
hasNextPage = it.hasNextPage
|
hasNextPage = it.hasNextPage
|
||||||
}
|
}
|
||||||
@@ -137,7 +154,7 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
model.searchResults.results.addAll(it.results)
|
model.searchResults.results.addAll(it.results)
|
||||||
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||||
|
|
||||||
progressAdapter.bar?.visibility = if (it.hasNextPage) View.VISIBLE else View.GONE
|
progressAdapter.bar?.isVisible = it.hasNextPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +168,10 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
} else
|
} else
|
||||||
headerAdaptor.requestFocus?.run()
|
headerAdaptor.requestFocus?.run()
|
||||||
|
|
||||||
if (intent.getBooleanExtra("search", false)) search()
|
if (intent.getBooleanExtra("search", false)) {
|
||||||
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED)
|
||||||
|
search()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import android.view.animation.AlphaAnimation
|
|||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.PopupMenu
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||||
@@ -28,7 +28,9 @@ import ani.dantotsu.openLinkInBrowser
|
|||||||
import ani.dantotsu.others.imagesearch.ImageSearchActivity
|
import ani.dantotsu.others.imagesearch.ImageSearchActivity
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import com.google.android.material.checkbox.MaterialCheckBox.*
|
import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED
|
||||||
|
import com.google.android.material.checkbox.MaterialCheckBox.STATE_INDETERMINATE
|
||||||
|
import com.google.android.material.checkbox.MaterialCheckBox.STATE_UNCHECKED
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -44,6 +46,20 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
||||||
private lateinit var binding: ItemSearchHeaderBinding
|
private lateinit var binding: ItemSearchHeaderBinding
|
||||||
|
|
||||||
|
private fun updateFilterTextViewDrawable() {
|
||||||
|
val filterDrawable = when (activity.result.sort) {
|
||||||
|
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
|
||||||
|
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
|
||||||
|
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
|
||||||
|
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
|
||||||
|
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
|
||||||
|
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
|
||||||
|
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
|
||||||
|
else -> R.drawable.ic_round_filter_alt_24
|
||||||
|
}
|
||||||
|
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||||
val binding =
|
val binding =
|
||||||
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
@@ -92,7 +108,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
binding.searchAdultCheck.isChecked = adult
|
binding.searchAdultCheck.isChecked = adult
|
||||||
binding.searchList.isChecked = listOnly == true
|
binding.searchList.isChecked = listOnly == true
|
||||||
|
|
||||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
|
||||||
activity.updateChips = { it.update() }
|
activity.updateChips = { it.update() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +118,65 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
binding.searchFilter.setOnClickListener {
|
binding.searchFilter.setOnClickListener {
|
||||||
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
|
binding.searchFilter.setOnLongClickListener {
|
||||||
|
val popupMenu = PopupMenu(activity, binding.searchFilter)
|
||||||
|
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
|
||||||
|
popupMenu.setOnMenuItemClickListener { item ->
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.sort_by_score -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[0]
|
||||||
|
activity.updateChips.invoke()
|
||||||
|
activity.search()
|
||||||
|
updateFilterTextViewDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_popular -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[1]
|
||||||
|
activity.updateChips.invoke()
|
||||||
|
activity.search()
|
||||||
|
updateFilterTextViewDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_trending -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[2]
|
||||||
|
activity.updateChips.invoke()
|
||||||
|
activity.search()
|
||||||
|
updateFilterTextViewDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_recent -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[3]
|
||||||
|
activity.updateChips.invoke()
|
||||||
|
activity.search()
|
||||||
|
updateFilterTextViewDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_a_z -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[4]
|
||||||
|
activity.updateChips.invoke()
|
||||||
|
activity.search()
|
||||||
|
updateFilterTextViewDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_z_a -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[5]
|
||||||
|
activity.updateChips.invoke()
|
||||||
|
activity.search()
|
||||||
|
updateFilterTextViewDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_pure_pain -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[6]
|
||||||
|
activity.updateChips.invoke()
|
||||||
|
activity.search()
|
||||||
|
updateFilterTextViewDrawable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
popupMenu.show()
|
||||||
|
true
|
||||||
|
}
|
||||||
binding.searchByImage.setOnClickListener {
|
binding.searchByImage.setOnClickListener {
|
||||||
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
|
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
|
||||||
}
|
}
|
||||||
@@ -230,14 +305,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
private fun fadeInAnimation(): Animation {
|
private fun fadeInAnimation(): Animation {
|
||||||
return AlphaAnimation(0f, 1f).apply {
|
return AlphaAnimation(0f, 1f).apply {
|
||||||
duration = 150
|
duration = 150
|
||||||
fillAfter = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fadeOutAnimation(): Animation {
|
private fun fadeOutAnimation(): Animation {
|
||||||
return AlphaAnimation(1f, 0f).apply {
|
return AlphaAnimation(1f, 0f).apply {
|
||||||
duration = 150
|
duration = 150
|
||||||
fillAfter = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +329,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchChipAdapter(val activity: SearchActivity) :
|
class SearchChipAdapter(
|
||||||
|
val activity: SearchActivity,
|
||||||
|
private val searchAdapter: SearchAdapter
|
||||||
|
) :
|
||||||
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||||
private var chips = activity.result.toChipList()
|
private var chips = activity.result.toChipList()
|
||||||
|
|
||||||
@@ -273,11 +349,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
|
||||||
val chip = chips[position]
|
val chip = chips[position]
|
||||||
holder.binding.root.apply {
|
holder.binding.root.apply {
|
||||||
text = chip.text
|
text = chip.text.replace("_", " ")
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
activity.result.removeChip(chip)
|
activity.result.removeChip(chip)
|
||||||
update()
|
update()
|
||||||
activity.search()
|
activity.search()
|
||||||
|
searchAdapter.updateFilterTextViewDrawable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,6 +363,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
fun update() {
|
fun update() {
|
||||||
chips = activity.result.toChipList()
|
chips = activity.result.toChipList()
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
|
searchAdapter.updateFilterTextViewDrawable()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = chips.size
|
override fun getItemCount(): Int = chips.size
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.GONE
|
import android.view.View.GONE
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.PopupMenu
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -17,6 +21,9 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||||
@@ -38,6 +45,54 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
|||||||
private var exGenres = mutableListOf<String>()
|
private var exGenres = mutableListOf<String>()
|
||||||
private var selectedTags = mutableListOf<String>()
|
private var selectedTags = mutableListOf<String>()
|
||||||
private var exTags = mutableListOf<String>()
|
private var exTags = mutableListOf<String>()
|
||||||
|
private fun updateChips() {
|
||||||
|
binding.searchFilterGenres.adapter?.notifyDataSetChanged()
|
||||||
|
binding.searchFilterTags.adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startBounceZoomAnimation(view: View? = null) {
|
||||||
|
val targetView = view ?: binding.sortByFilter
|
||||||
|
val bounceZoomAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
|
||||||
|
targetView.startAnimation(bounceZoomAnimation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSortByFilterImage() {
|
||||||
|
val filterDrawable = when (activity.result.sort) {
|
||||||
|
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
|
||||||
|
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
|
||||||
|
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
|
||||||
|
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
|
||||||
|
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
|
||||||
|
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
|
||||||
|
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
|
||||||
|
else -> R.drawable.ic_round_filter_alt_24
|
||||||
|
}
|
||||||
|
binding.sortByFilter.setImageResource(filterDrawable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetSearchFilter() {
|
||||||
|
activity.result.sort = null
|
||||||
|
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
|
||||||
|
startBounceZoomAnimation(binding.sortByFilter)
|
||||||
|
activity.result.countryOfOrigin = null
|
||||||
|
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
|
||||||
|
startBounceZoomAnimation(binding.countryFilter)
|
||||||
|
|
||||||
|
selectedGenres.clear()
|
||||||
|
exGenres.clear()
|
||||||
|
selectedTags.clear()
|
||||||
|
exTags.clear()
|
||||||
|
binding.searchStatus.setText("")
|
||||||
|
binding.searchSource.setText("")
|
||||||
|
binding.searchFormat.setText("")
|
||||||
|
binding.searchSeason.setText("")
|
||||||
|
binding.searchYear.setText("")
|
||||||
|
binding.searchStatus.clearFocus()
|
||||||
|
binding.searchFormat.clearFocus()
|
||||||
|
binding.searchSeason.clearFocus()
|
||||||
|
binding.searchYear.clearFocus()
|
||||||
|
updateChips()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
@@ -47,14 +102,157 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
|||||||
exGenres = activity.result.excludedGenres ?: mutableListOf()
|
exGenres = activity.result.excludedGenres ?: mutableListOf()
|
||||||
selectedTags = activity.result.tags ?: mutableListOf()
|
selectedTags = activity.result.tags ?: mutableListOf()
|
||||||
exTags = activity.result.excludedTags ?: mutableListOf()
|
exTags = activity.result.excludedTags ?: mutableListOf()
|
||||||
|
setSortByFilterImage()
|
||||||
|
|
||||||
|
binding.resetSearchFilter.setOnClickListener {
|
||||||
|
val rotateAnimation =
|
||||||
|
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
|
||||||
|
rotateAnimation.duration = 500
|
||||||
|
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
rotateAnimation.start()
|
||||||
|
resetSearchFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.resetSearchFilter.setOnLongClickListener {
|
||||||
|
val rotateAnimation =
|
||||||
|
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
|
||||||
|
rotateAnimation.duration = 500
|
||||||
|
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
rotateAnimation.start()
|
||||||
|
val bounceAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
|
||||||
|
|
||||||
|
binding.resetSearchFilter.startAnimation(bounceAnimation)
|
||||||
|
binding.resetSearchFilter.postDelayed({
|
||||||
|
resetSearchFilter()
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
activity.result.apply {
|
||||||
|
status =
|
||||||
|
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
|
||||||
|
source =
|
||||||
|
binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
|
||||||
|
format = binding.searchFormat.text.toString().ifBlank { null }
|
||||||
|
season = binding.searchSeason.text.toString().ifBlank { null }
|
||||||
|
startYear = binding.searchYear.text.toString().toIntOrNull()
|
||||||
|
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||||
|
sort = activity.result.sort
|
||||||
|
genres = selectedGenres
|
||||||
|
tags = selectedTags
|
||||||
|
excludedGenres = exGenres
|
||||||
|
excludedTags = exTags
|
||||||
|
}
|
||||||
|
activity.updateChips.invoke()
|
||||||
|
activity.search()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.sortByFilter.setOnClickListener {
|
||||||
|
val popupMenu = PopupMenu(requireContext(), it)
|
||||||
|
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
|
||||||
|
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.sort_by_score -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[0]
|
||||||
|
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
|
||||||
|
startBounceZoomAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_popular -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[1]
|
||||||
|
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
|
||||||
|
startBounceZoomAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_trending -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[2]
|
||||||
|
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
|
||||||
|
startBounceZoomAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_recent -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[3]
|
||||||
|
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
|
||||||
|
startBounceZoomAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_a_z -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[4]
|
||||||
|
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
|
||||||
|
startBounceZoomAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_z_a -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[5]
|
||||||
|
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
|
||||||
|
startBounceZoomAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.sort_by_pure_pain -> {
|
||||||
|
activity.result.sort = Anilist.sortBy[6]
|
||||||
|
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
|
||||||
|
startBounceZoomAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
popupMenu.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.countryFilter.setOnClickListener {
|
||||||
|
val popupMenu = PopupMenu(requireContext(), it)
|
||||||
|
popupMenu.menuInflater.inflate(R.menu.country_filter_menu, popupMenu.menu)
|
||||||
|
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.country_global -> {
|
||||||
|
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
|
||||||
|
startBounceZoomAnimation(binding.countryFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.country_china -> {
|
||||||
|
activity.result.countryOfOrigin = "CN"
|
||||||
|
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
|
||||||
|
startBounceZoomAnimation(binding.countryFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.country_south_korea -> {
|
||||||
|
activity.result.countryOfOrigin = "KR"
|
||||||
|
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
|
||||||
|
startBounceZoomAnimation(binding.countryFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.country_japan -> {
|
||||||
|
activity.result.countryOfOrigin = "JP"
|
||||||
|
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
|
||||||
|
startBounceZoomAnimation(binding.countryFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.country_taiwan -> {
|
||||||
|
activity.result.countryOfOrigin = "TW"
|
||||||
|
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
|
||||||
|
startBounceZoomAnimation(binding.countryFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
popupMenu.show()
|
||||||
|
}
|
||||||
|
|
||||||
binding.searchFilterApply.setOnClickListener {
|
binding.searchFilterApply.setOnClickListener {
|
||||||
activity.result.apply {
|
activity.result.apply {
|
||||||
|
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
|
||||||
|
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
|
||||||
format = binding.searchFormat.text.toString().ifBlank { null }
|
format = binding.searchFormat.text.toString().ifBlank { null }
|
||||||
sort = binding.searchSortBy.text.toString().ifBlank { null }
|
|
||||||
?.let { Anilist.sortBy[resources.getStringArray(R.array.sort_by).indexOf(it)] }
|
|
||||||
season = binding.searchSeason.text.toString().ifBlank { null }
|
season = binding.searchSeason.text.toString().ifBlank { null }
|
||||||
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
if (activity.result.type == "ANIME") {
|
||||||
|
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||||
|
} else {
|
||||||
|
startYear = binding.searchYear.text.toString().toIntOrNull()
|
||||||
|
}
|
||||||
|
sort = activity.result.sort
|
||||||
|
countryOfOrigin = activity.result.countryOfOrigin
|
||||||
genres = selectedGenres
|
genres = selectedGenres
|
||||||
tags = selectedTags
|
tags = selectedTags
|
||||||
excludedGenres = exGenres
|
excludedGenres = exGenres
|
||||||
@@ -67,15 +265,23 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
|||||||
binding.searchFilterCancel.setOnClickListener {
|
binding.searchFilterCancel.setOnClickListener {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
val format =
|
||||||
binding.searchSortBy.setText(activity.result.sort?.let {
|
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
|
||||||
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
|
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
|
||||||
})
|
binding.searchStatus.setAdapter(
|
||||||
binding.searchSortBy.setAdapter(
|
|
||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
binding.root.context,
|
binding.root.context,
|
||||||
R.layout.item_dropdown,
|
R.layout.item_dropdown,
|
||||||
resources.getStringArray(R.array.sort_by)
|
format
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.searchSource.setText(activity.result.source?.replace("_", " "))
|
||||||
|
binding.searchSource.setAdapter(
|
||||||
|
ArrayAdapter(
|
||||||
|
binding.root.context,
|
||||||
|
R.layout.item_dropdown,
|
||||||
|
Anilist.source.toTypedArray()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,11 +290,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
|||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
binding.root.context,
|
binding.root.context,
|
||||||
R.layout.item_dropdown,
|
R.layout.item_dropdown,
|
||||||
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
|
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
|
if (activity.result.type == "ANIME") {
|
||||||
|
binding.searchYear.setText(activity.result.seasonYear?.toString())
|
||||||
|
} else {
|
||||||
|
binding.searchYear.setText(activity.result.startYear?.toString())
|
||||||
|
}
|
||||||
|
binding.searchYear.setAdapter(
|
||||||
|
ArrayAdapter(
|
||||||
|
binding.root.context,
|
||||||
|
R.layout.item_dropdown,
|
||||||
|
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
|
||||||
|
.reversed().toTypedArray()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
|
||||||
else {
|
else {
|
||||||
binding.searchSeason.setText(activity.result.season)
|
binding.searchSeason.setText(activity.result.season)
|
||||||
binding.searchSeason.setAdapter(
|
binding.searchSeason.setAdapter(
|
||||||
@@ -98,16 +318,6 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
|||||||
Anilist.seasons.toTypedArray()
|
Anilist.seasons.toTypedArray()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.searchYear.setText(activity.result.seasonYear?.toString())
|
|
||||||
binding.searchYear.setAdapter(
|
|
||||||
ArrayAdapter(
|
|
||||||
binding.root.context,
|
|
||||||
R.layout.item_dropdown,
|
|
||||||
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
|
|
||||||
.reversed().toTypedArray()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->
|
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -22,7 +21,6 @@ abstract class SourceAdapter(
|
|||||||
return SourceViewHolder(binding)
|
return SourceViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val character = sources[position]
|
val character = sources[position]
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||||||
i = media!!.selected!!.sourceIndex
|
i = media!!.selected!!.sourceIndex
|
||||||
|
|
||||||
val source = if (media!!.anime != null) {
|
val source = if (media!!.anime != null) {
|
||||||
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
|
(if (media!!.isAdult) HAnimeSources else AnimeSources)[i!!]
|
||||||
} else {
|
} else {
|
||||||
anime = false
|
anime = false
|
||||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -114,7 +115,7 @@ class StudioActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
binding.studioProgressBar.isGone = loaded
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,19 +5,19 @@ import ani.dantotsu.download.DownloadedType
|
|||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.parsers.SubtitleType
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import com.anggrayudi.storage.file.openOutputStream
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class SubtitleDownloader {
|
class SubtitleDownloader {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//doesn't really download the subtitles -\_(o_o)_/-
|
//doesn't really download the subtitles -\_(o_o)_/-
|
||||||
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
|
suspend fun loadSubtitleType(url: String): SubtitleType =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
@@ -51,21 +51,17 @@ class SubtitleDownloader {
|
|||||||
downloadedType: DownloadedType
|
downloadedType: DownloadedType
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val directory = DownloadsManager.getDirectory(
|
val directory = DownloadsManager.getSubDirectory(
|
||||||
context,
|
context,
|
||||||
downloadedType.type,
|
downloadedType.type,
|
||||||
downloadedType.title,
|
false,
|
||||||
downloadedType.chapter
|
downloadedType.titleName,
|
||||||
)
|
downloadedType.chapterName
|
||||||
if (!directory.exists()) { //just in case
|
) ?: throw Exception("Could not create directory")
|
||||||
directory.mkdirs()
|
val type = loadSubtitleType(url)
|
||||||
}
|
directory.findFile("subtitle.${type}")?.delete()
|
||||||
val type = loadSubtitleType(context, url)
|
val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
|
||||||
val subtiteFile = File(directory, "subtitle.${type}")
|
?: throw Exception("Could not create subtitle file")
|
||||||
if (subtiteFile.exists()) {
|
|
||||||
subtiteFile.delete()
|
|
||||||
}
|
|
||||||
subtiteFile.createNewFile()
|
|
||||||
|
|
||||||
val client = Injekt.get<NetworkHelper>().client
|
val client = Injekt.get<NetworkHelper>().client
|
||||||
val request = Request.Builder().url(url).build()
|
val request = Request.Builder().url(url).build()
|
||||||
@@ -77,7 +73,8 @@ class SubtitleDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reponse.body.byteStream().use { input ->
|
reponse.body.byteStream().use { input ->
|
||||||
subtiteFile.outputStream().use { output ->
|
subtitleFile.openOutputStream(context, false).use { output ->
|
||||||
|
if (output == null) throw Exception("Could not open output stream")
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
package ani.dantotsu.media
|
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import ani.dantotsu.R
|
|
||||||
import ani.dantotsu.navBarHeight
|
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
|
||||||
|
|
||||||
class TripleNavAdapter(
|
|
||||||
private val nav1: AnimatedBottomBar,
|
|
||||||
private val nav2: AnimatedBottomBar,
|
|
||||||
private val nav3: AnimatedBottomBar,
|
|
||||||
anime: Boolean,
|
|
||||||
format: String,
|
|
||||||
private val isScreenVertical: Boolean = false
|
|
||||||
) {
|
|
||||||
var selected: Int = 0
|
|
||||||
var selectionListener: ((Int, Int) -> Unit)? = null
|
|
||||||
init {
|
|
||||||
nav1.tabs.clear()
|
|
||||||
nav2.tabs.clear()
|
|
||||||
nav3.tabs.clear()
|
|
||||||
val infoTab = nav1.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
|
|
||||||
val watchTab = if (anime) {
|
|
||||||
nav2.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
|
|
||||||
} else if (format == "NOVEL") {
|
|
||||||
nav2.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
|
|
||||||
} else {
|
|
||||||
nav2.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
|
|
||||||
}
|
|
||||||
val commentTab = nav3.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
|
|
||||||
nav1.addTab(infoTab)
|
|
||||||
nav1.visibility = ViewGroup.VISIBLE
|
|
||||||
if (isScreenVertical) {
|
|
||||||
nav2.visibility = ViewGroup.GONE
|
|
||||||
nav3.visibility = ViewGroup.GONE
|
|
||||||
nav1.addTab(watchTab)
|
|
||||||
nav1.addTab(commentTab)
|
|
||||||
nav1.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = navBarHeight
|
|
||||||
}
|
|
||||||
nav2.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = navBarHeight
|
|
||||||
}
|
|
||||||
nav3.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = navBarHeight
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nav1.indicatorColor = Color.TRANSPARENT
|
|
||||||
nav2.indicatorColor = Color.TRANSPARENT
|
|
||||||
nav3.indicatorColor = Color.TRANSPARENT
|
|
||||||
nav2.visibility = ViewGroup.VISIBLE
|
|
||||||
nav3.visibility = ViewGroup.VISIBLE
|
|
||||||
nav2.addTab(watchTab)
|
|
||||||
nav3.addTab(commentTab)
|
|
||||||
nav2.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
|
||||||
override fun onTabSelected(
|
|
||||||
lastIndex: Int,
|
|
||||||
lastTab: AnimatedBottomBar.Tab?,
|
|
||||||
newIndex: Int,
|
|
||||||
newTab: AnimatedBottomBar.Tab
|
|
||||||
) {
|
|
||||||
selected = 1
|
|
||||||
deselectOthers(selected)
|
|
||||||
selectionListener?.invoke(selected, newTab.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
nav3.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
|
||||||
override fun onTabSelected(
|
|
||||||
lastIndex: Int,
|
|
||||||
lastTab: AnimatedBottomBar.Tab?,
|
|
||||||
newIndex: Int,
|
|
||||||
newTab: AnimatedBottomBar.Tab
|
|
||||||
) {
|
|
||||||
selected = 2
|
|
||||||
deselectOthers(selected)
|
|
||||||
selectionListener?.invoke(selected, newTab.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
nav1.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
|
||||||
override fun onTabSelected(
|
|
||||||
lastIndex: Int,
|
|
||||||
lastTab: AnimatedBottomBar.Tab?,
|
|
||||||
newIndex: Int,
|
|
||||||
newTab: AnimatedBottomBar.Tab
|
|
||||||
) {
|
|
||||||
if (!isScreenVertical) {
|
|
||||||
selected = 0
|
|
||||||
deselectOthers(selected)
|
|
||||||
} else selected = newIndex
|
|
||||||
selectionListener?.invoke(selected, newTab.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deselectOthers(selected: Int) {
|
|
||||||
if (selected == 0) {
|
|
||||||
nav2.clearSelection()
|
|
||||||
nav3.clearSelection()
|
|
||||||
}
|
|
||||||
if (selected == 1) {
|
|
||||||
nav1.clearSelection()
|
|
||||||
nav3.clearSelection()
|
|
||||||
}
|
|
||||||
if (selected == 2) {
|
|
||||||
nav1.clearSelection()
|
|
||||||
nav2.clearSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun selectTab(tab: Int) {
|
|
||||||
selected = tab
|
|
||||||
if (!isScreenVertical) {
|
|
||||||
when (tab) {
|
|
||||||
0 -> nav1.selectTabAt(0)
|
|
||||||
1 -> nav2.selectTabAt(0)
|
|
||||||
2 -> nav3.selectTabAt(0)
|
|
||||||
}
|
|
||||||
deselectOthers(selected)
|
|
||||||
} else {
|
|
||||||
nav1.selectTabAt(selected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setVisibility(visibility: Int) {
|
|
||||||
if (isScreenVertical) {
|
|
||||||
nav1.visibility = visibility
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nav1.visibility = visibility
|
|
||||||
nav2.visibility = visibility
|
|
||||||
nav3.visibility = visibility
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
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|episodio|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
|
||||||
const val failedEpisodeNumberRegex =
|
|
||||||
"(?<!part\\s)\\b(\\d+)\\b"
|
|
||||||
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
|
||||||
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
|
||||||
|
|
||||||
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
|
||||||
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
|
||||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
|
||||||
|
|
||||||
return if (subdubMatcher.find()) {
|
|
||||||
val soft = subdubMatcher.group(1)
|
|
||||||
val subdub = subdubMatcher.group(2)
|
|
||||||
val bed = subdubMatcher.group(3) ?: ""
|
|
||||||
|
|
||||||
val toggled = when (typeToSetTo) {
|
|
||||||
SubDubType.SUB -> "sub"
|
|
||||||
SubDubType.DUB -> "dub"
|
|
||||||
SubDubType.NULL -> ""
|
|
||||||
}
|
|
||||||
val toggledCasePreserved =
|
|
||||||
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
|
|
||||||
?.isUpperCase() == true
|
|
||||||
) toggled.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) it.titlecase(
|
|
||||||
Locale.ROOT
|
|
||||||
) else it.toString()
|
|
||||||
} else toggled
|
|
||||||
|
|
||||||
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSubDub(text: String): SubDubType {
|
|
||||||
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
|
||||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
|
||||||
|
|
||||||
return if (subdubMatcher.find()) {
|
|
||||||
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
|
|
||||||
when (subdub) {
|
|
||||||
"sub" -> SubDubType.SUB
|
|
||||||
"dub" -> SubDubType.DUB
|
|
||||||
else -> SubDubType.NULL
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SubDubType.NULL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class SubDubType {
|
|
||||||
SUB, DUB, NULL
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findSeasonNumber(text: String): Int? {
|
|
||||||
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
|
||||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
|
||||||
|
|
||||||
return if (seasonMatcher.find()) {
|
|
||||||
seasonMatcher.group(2)?.toInt()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findEpisodeNumber(text: String): Float? {
|
|
||||||
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
|
|
||||||
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
|
||||||
|
|
||||||
return if (episodeMatcher.find()) {
|
|
||||||
if (episodeMatcher.group(2) != null) {
|
|
||||||
episodeMatcher.group(2)?.toFloat()
|
|
||||||
} else {
|
|
||||||
val failedEpisodeNumberPattern: Pattern =
|
|
||||||
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
|
|
||||||
val failedEpisodeNumberMatcher: Matcher =
|
|
||||||
failedEpisodeNumberPattern.matcher(text)
|
|
||||||
if (failedEpisodeNumberMatcher.find()) {
|
|
||||||
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeEpisodeNumber(text: String): String {
|
|
||||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
|
||||||
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
val letterPattern = Regex("[a-zA-Z]")
|
|
||||||
return if (letterPattern.containsMatchIn(removedNumber)) {
|
|
||||||
removedNumber
|
|
||||||
} else {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun removeEpisodeNumberCompletely(text: String): String {
|
|
||||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
|
||||||
val removedNumber = text.replace(regexPattern, "")
|
|
||||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
|
||||||
val failedEpisodeNumberPattern: Regex =
|
|
||||||
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
|
||||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
|
||||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
removedNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import ani.dantotsu.settings.FAQActivity
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
@@ -13,22 +11,34 @@ import android.widget.LinearLayout
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.FileUrl
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
|
import ani.dantotsu.displayTimer
|
||||||
|
import ani.dantotsu.isOnline
|
||||||
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||||
|
import ani.dantotsu.openSettings
|
||||||
import ani.dantotsu.others.LanguageMapper
|
import ani.dantotsu.others.LanguageMapper
|
||||||
import ani.dantotsu.others.webview.CookieCatcher
|
import ani.dantotsu.others.webview.CookieCatcher
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||||
import ani.dantotsu.parsers.WatchSources
|
import ani.dantotsu.parsers.WatchSources
|
||||||
|
import ani.dantotsu.px
|
||||||
|
import ani.dantotsu.settings.FAQActivity
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.toast
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
|
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
|
||||||
@@ -42,7 +52,7 @@ class AnimeWatchAdapter(
|
|||||||
private val fragment: AnimeWatchFragment,
|
private val fragment: AnimeWatchFragment,
|
||||||
private val watchSources: WatchSources
|
private val watchSources: WatchSources
|
||||||
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
|
||||||
|
private var autoSelect = true
|
||||||
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
||||||
private var _binding: ItemAnimeWatchBinding? = null
|
private var _binding: ItemAnimeWatchBinding? = null
|
||||||
|
|
||||||
@@ -54,7 +64,6 @@ class AnimeWatchAdapter(
|
|||||||
private var nestedDialog: AlertDialog? = null
|
private var nestedDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
_binding = binding
|
_binding = binding
|
||||||
@@ -97,15 +106,12 @@ class AnimeWatchAdapter(
|
|||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal(
|
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
||||||
PrefName.OfflineMode
|
|
||||||
)
|
|
||||||
) View.GONE else View.VISIBLE
|
|
||||||
|
|
||||||
binding.animeSourceNameContainer.visibility = offline
|
binding.animeSourceNameContainer.isGone = offline
|
||||||
binding.animeSourceSettings.visibility = offline
|
binding.animeSourceSettings.isGone = offline
|
||||||
binding.animeSourceSearch.visibility = offline
|
binding.animeSourceSearch.isGone = offline
|
||||||
binding.animeSourceTitle.visibility = offline
|
binding.animeSourceTitle.isGone = offline
|
||||||
|
|
||||||
//Source Selection
|
//Source Selection
|
||||||
var source =
|
var source =
|
||||||
@@ -117,8 +123,7 @@ class AnimeWatchAdapter(
|
|||||||
this.selectDub = media.selected!!.preferDub
|
this.selectDub = media.selected!!.preferDub
|
||||||
binding.animeSourceTitle.text = showUserText
|
binding.animeSourceTitle.text = showUserText
|
||||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
binding.animeSourceDubbedCont.visibility =
|
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +142,7 @@ class AnimeWatchAdapter(
|
|||||||
changing = true
|
changing = true
|
||||||
binding.animeSourceDubbed.isChecked = selectDub
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
changing = false
|
changing = false
|
||||||
binding.animeSourceDubbedCont.visibility =
|
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
|
||||||
source = i
|
source = i
|
||||||
setLanguageList(0, i)
|
setLanguageList(0, i)
|
||||||
}
|
}
|
||||||
@@ -158,14 +162,12 @@ class AnimeWatchAdapter(
|
|||||||
changing = true
|
changing = true
|
||||||
binding.animeSourceDubbed.isChecked = selectDub
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
changing = false
|
changing = false
|
||||||
binding.animeSourceDubbedCont.visibility =
|
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
|
||||||
setLanguageList(i, source)
|
setLanguageList(i, source)
|
||||||
}
|
}
|
||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
|
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
|
||||||
} ?: run {
|
} ?: run { }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//settings
|
//settings
|
||||||
@@ -223,9 +225,9 @@ class AnimeWatchAdapter(
|
|||||||
else -> dialogBinding.animeSourceList
|
else -> dialogBinding.animeSourceList
|
||||||
}
|
}
|
||||||
when (style) {
|
when (style) {
|
||||||
0 -> dialogBinding.layoutText.text = "List"
|
0 -> dialogBinding.layoutText.setText(R.string.list)
|
||||||
1 -> dialogBinding.layoutText.text = "Grid"
|
1 -> dialogBinding.layoutText.setText(R.string.grid)
|
||||||
2 -> dialogBinding.layoutText.text = "Compact"
|
2 -> dialogBinding.layoutText.setText(R.string.compact)
|
||||||
else -> dialogBinding.animeSourceList
|
else -> dialogBinding.animeSourceList
|
||||||
}
|
}
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
@@ -237,24 +239,24 @@ class AnimeWatchAdapter(
|
|||||||
dialogBinding.animeSourceList.setOnClickListener {
|
dialogBinding.animeSourceList.setOnClickListener {
|
||||||
selected(it as ImageButton)
|
selected(it as ImageButton)
|
||||||
style = 0
|
style = 0
|
||||||
dialogBinding.layoutText.text = "List"
|
dialogBinding.layoutText.setText(R.string.list)
|
||||||
run = true
|
run = true
|
||||||
}
|
}
|
||||||
dialogBinding.animeSourceGrid.setOnClickListener {
|
dialogBinding.animeSourceGrid.setOnClickListener {
|
||||||
selected(it as ImageButton)
|
selected(it as ImageButton)
|
||||||
style = 1
|
style = 1
|
||||||
dialogBinding.layoutText.text = "Grid"
|
dialogBinding.layoutText.setText(R.string.grid)
|
||||||
run = true
|
run = true
|
||||||
}
|
}
|
||||||
dialogBinding.animeSourceCompact.setOnClickListener {
|
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||||
selected(it as ImageButton)
|
selected(it as ImageButton)
|
||||||
style = 2
|
style = 2
|
||||||
dialogBinding.layoutText.text = "Compact"
|
dialogBinding.layoutText.setText(R.string.compact)
|
||||||
run = true
|
run = true
|
||||||
}
|
}
|
||||||
dialogBinding.animeWebviewContainer.setOnClickListener {
|
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||||
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||||
toast("WebView not installed")
|
toast(R.string.webview_not_installed)
|
||||||
}
|
}
|
||||||
//start CookieCatcher activity
|
//start CookieCatcher activity
|
||||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||||
@@ -307,7 +309,6 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Chips
|
//Chips
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
|
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
|
||||||
val binding = _binding
|
val binding = _binding
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
@@ -329,7 +330,9 @@ class AnimeWatchAdapter(
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
|
||||||
|
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||||
|
chip.text = chipText
|
||||||
chip.setTextColor(
|
chip.setTextColor(
|
||||||
ContextCompat.getColorStateList(
|
ContextCompat.getColorStateList(
|
||||||
fragment.requireContext(),
|
fragment.requireContext(),
|
||||||
@@ -363,7 +366,6 @@ class AnimeWatchAdapter(
|
|||||||
_binding?.animeSourceChipGroup?.removeAllViews()
|
_binding?.animeSourceChipGroup?.removeAllViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
fun handleEpisodes() {
|
fun handleEpisodes() {
|
||||||
val binding = _binding
|
val binding = _binding
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
@@ -371,9 +373,9 @@ class AnimeWatchAdapter(
|
|||||||
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
||||||
|
|
||||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||||
val appEp =
|
val appEp = PrefManager.getCustomVal<String?>(
|
||||||
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
|
"${media.id}_current_ep", ""
|
||||||
?: 1
|
)?.toIntOrNull() ?: 1
|
||||||
|
|
||||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||||
if (episodes.contains(continueEp)) {
|
if (episodes.contains(continueEp)) {
|
||||||
@@ -403,21 +405,27 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
val ep = media.anime.episodes!![continueEp]!!
|
val ep = media.anime.episodes!![continueEp]!!
|
||||||
|
|
||||||
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
|
||||||
|
|
||||||
binding.itemEpisodeImage.loadImage(
|
binding.itemEpisodeImage.loadImage(
|
||||||
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
||||||
)
|
)
|
||||||
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
||||||
|
|
||||||
binding.animeSourceContinueText.text =
|
binding.animeSourceContinueText.text =
|
||||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
|
currActivity()!!.getString(
|
||||||
|
R.string.continue_episode, ep.number, if (ep.filler)
|
||||||
|
currActivity()!!.getString(R.string.filler_tag)
|
||||||
|
else
|
||||||
|
"", cleanedTitle
|
||||||
|
)
|
||||||
binding.animeSourceContinue.setOnClickListener {
|
binding.animeSourceContinue.setOnClickListener {
|
||||||
fragment.onEpisodeClick(continueEp)
|
fragment.onEpisodeClick(continueEp)
|
||||||
}
|
}
|
||||||
if (fragment.continueEp) {
|
if (fragment.continueEp) {
|
||||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
|
if (
|
||||||
PrefName.WatchPercentage
|
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
|
||||||
)
|
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
|
||||||
) {
|
) {
|
||||||
binding.animeSourceContinue.performClick()
|
binding.animeSourceContinue.performClick()
|
||||||
fragment.continueEp = false
|
fragment.continueEp = false
|
||||||
@@ -428,13 +436,31 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSourceProgressBar.visibility = View.GONE
|
binding.animeSourceProgressBar.visibility = View.GONE
|
||||||
if (media.anime.episodes!!.isNotEmpty()) {
|
|
||||||
binding.animeSourceNotFound.visibility = View.GONE
|
val sourceFound = media.anime.episodes!!.isNotEmpty()
|
||||||
binding.faqbutton.visibility = View.GONE}
|
binding.animeSourceNotFound.isGone = sourceFound
|
||||||
else {
|
binding.faqbutton.isGone = sourceFound
|
||||||
binding.animeSourceNotFound.visibility = View.VISIBLE
|
|
||||||
binding.faqbutton.visibility = View.VISIBLE
|
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
|
||||||
|
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
|
||||||
|
val nextIndex = media.selected!!.sourceIndex + 1
|
||||||
|
binding.animeSource.setText(
|
||||||
|
binding.animeSource.adapter
|
||||||
|
.getItem(nextIndex).toString(), false
|
||||||
|
)
|
||||||
|
fragment.onSourceChange(nextIndex).apply {
|
||||||
|
binding.animeSourceTitle.text = showUserText
|
||||||
|
showUserTextListener =
|
||||||
|
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
|
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||||
|
setLanguageList(0, nextIndex)
|
||||||
|
}
|
||||||
|
subscribeButton(false)
|
||||||
|
fragment.loadEpisodes(nextIndex, false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
binding.animeSource.setOnClickListener { autoSelect = false }
|
||||||
} else {
|
} else {
|
||||||
binding.animeSourceContinue.visibility = View.GONE
|
binding.animeSourceContinue.visibility = View.GONE
|
||||||
binding.animeSourceNotFound.visibility = View.GONE
|
binding.animeSourceNotFound.visibility = View.GONE
|
||||||
@@ -480,8 +506,7 @@ class AnimeWatchAdapter(
|
|||||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
//Timer
|
displayTimer(media, binding.animeSourceContainer)
|
||||||
countDown(media, binding.animeSourceContainer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,34 +17,45 @@ import androidx.annotation.OptIn
|
|||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.math.MathUtils
|
import androidx.core.math.MathUtils
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.offline.DownloadService
|
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.FileUrl
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
import ani.dantotsu.dp
|
||||||
|
import ani.dantotsu.isOnline
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||||
|
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
|
||||||
import ani.dantotsu.others.LanguageMapper
|
import ani.dantotsu.others.LanguageMapper
|
||||||
import ani.dantotsu.parsers.AnimeParser
|
import ani.dantotsu.parsers.AnimeParser
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.HAnimeSources
|
import ani.dantotsu.parsers.HAnimeSources
|
||||||
|
import ani.dantotsu.setNavigationTheme
|
||||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
|
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
|
||||||
|
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
@@ -188,10 +199,16 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
awaitAll(
|
val offline =
|
||||||
async { model.loadKitsuEpisodes(media) },
|
!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
||||||
async { model.loadFillerEpisodes(media) }
|
if (offline) {
|
||||||
)
|
media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex
|
||||||
|
} else {
|
||||||
|
awaitAll(
|
||||||
|
async { model.loadKitsuEpisodes(media) },
|
||||||
|
async { model.loadFillerEpisodes(media) }
|
||||||
|
)
|
||||||
|
}
|
||||||
model.loadEpisodes(media, media.selected!!.sourceIndex)
|
model.loadEpisodes(media, media.selected!!.sourceIndex)
|
||||||
}
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
@@ -216,7 +233,7 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||||
episode.desc =
|
episode.desc =
|
||||||
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||||
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
|
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
|
||||||
episode.title ?: ""
|
episode.title ?: ""
|
||||||
).isBlank()
|
).isBlank()
|
||||||
) media.anime!!.kitsuEpisodes!![i]?.title
|
) media.anime!!.kitsuEpisodes!![i]?.title
|
||||||
@@ -340,16 +357,12 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||||
val activity = activity
|
val activity = activity
|
||||||
if (activity is MediaDetailsActivity && isAdded) {
|
if (activity is MediaDetailsActivity && isAdded) {
|
||||||
val visibility = if (show) View.VISIBLE else View.GONE
|
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).isVisible = show
|
||||||
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
activity.findViewById<ViewPager2>(R.id.mediaViewPager).isVisible = show
|
||||||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
activity.findViewById<CardView>(R.id.mediaCover).isVisible = show
|
||||||
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
activity.findViewById<CardView>(R.id.mediaClose).isVisible = show
|
||||||
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
activity.navBar.isVisible = show
|
||||||
|
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).isGone = show
|
||||||
activity.tabLayout.setVisibility(visibility)
|
|
||||||
|
|
||||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
|
||||||
if (show) View.GONE else View.VISIBLE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var itemSelected = false
|
var itemSelected = false
|
||||||
@@ -417,7 +430,29 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onAnimeEpisodeDownloadClick(i: String) {
|
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
activity?.let {
|
||||||
|
if (!hasDirAccess(it)) {
|
||||||
|
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
|
||||||
|
if (success) {
|
||||||
|
model.onEpisodeClick(
|
||||||
|
media,
|
||||||
|
i,
|
||||||
|
requireActivity().supportFragmentManager,
|
||||||
|
isDownload = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
snackString(getString(R.string.download_permission_required))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model.onEpisodeClick(
|
||||||
|
media,
|
||||||
|
i,
|
||||||
|
requireActivity().supportFragmentManager,
|
||||||
|
isDownload = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
||||||
@@ -435,10 +470,11 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
i,
|
i,
|
||||||
DownloadedType.Type.ANIME
|
MediaType.ANIME
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
episodeAdapter.purgeDownload(i)
|
episodeAdapter.purgeDownload(i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@@ -447,22 +483,13 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
i,
|
i,
|
||||||
DownloadedType.Type.ANIME
|
MediaType.ANIME
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
||||||
taskName,
|
episodeAdapter.deleteDownload(i)
|
||||||
""
|
}
|
||||||
) ?: ""
|
|
||||||
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
|
||||||
DownloadService.sendRemoveDownload(
|
|
||||||
requireContext(),
|
|
||||||
ExoplayerDownloadService::class.java,
|
|
||||||
id,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
episodeAdapter.deleteDownload(i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||||
@@ -526,8 +553,8 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
||||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||||
for (download in downloadManager.animeDownloadedTypes) {
|
for (download in downloadManager.animeDownloadedTypes) {
|
||||||
if (download.title == media.mainName()) {
|
if (media.compareName(download.titleName)) {
|
||||||
episodeAdapter.stopDownload(download.chapter)
|
episodeAdapter.stopDownload(download.chapterName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -8,18 +7,21 @@ import android.view.ViewGroup
|
|||||||
import android.view.animation.LinearInterpolator
|
import android.view.animation.LinearInterpolator
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.offline.DownloadIndex
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.updateProgress
|
import ani.dantotsu.connections.updateProgress
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
import ani.dantotsu.download.DownloadsManager.Companion.getDirSize
|
||||||
import ani.dantotsu.download.video.Helper
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.setAnimation
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
@@ -53,15 +55,7 @@ class EpisodeAdapter(
|
|||||||
var arr: List<Episode> = arrayListOf(),
|
var arr: List<Episode> = arrayListOf(),
|
||||||
var offlineMode: Boolean
|
var offlineMode: Boolean
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
val context = fragment.requireContext()
|
||||||
private lateinit var index: DownloadIndex
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (offlineMode) {
|
|
||||||
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return (when (viewType) {
|
return (when (viewType) {
|
||||||
@@ -97,11 +91,10 @@ class EpisodeAdapter(
|
|||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
val ep = arr[position]
|
val ep = arr[position]
|
||||||
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
|
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
|
||||||
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
|
||||||
} else {
|
} else {
|
||||||
ep.number
|
ep.number
|
||||||
} ?: ""
|
} ?: ""
|
||||||
@@ -125,8 +118,7 @@ class EpisodeAdapter(
|
|||||||
binding.itemEpisodeFiller.visibility = View.GONE
|
binding.itemEpisodeFiller.visibility = View.GONE
|
||||||
binding.itemEpisodeFillerView.visibility = View.GONE
|
binding.itemEpisodeFillerView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
binding.itemEpisodeDesc.visibility =
|
binding.itemEpisodeDesc.isVisible = !ep.desc.isNullOrBlank()
|
||||||
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
|
|
||||||
binding.itemEpisodeDesc.text = ep.desc ?: ""
|
binding.itemEpisodeDesc.text = ep.desc ?: ""
|
||||||
holder.bind(ep.number, ep.downloadProgress, ep.desc)
|
holder.bind(ep.number, ep.downloadProgress, ep.desc)
|
||||||
|
|
||||||
@@ -203,8 +195,7 @@ class EpisodeAdapter(
|
|||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||||
binding.itemEpisodeNumber.text = ep.number
|
binding.itemEpisodeNumber.text = ep.number
|
||||||
binding.itemEpisodeFillerView.visibility =
|
binding.itemEpisodeFillerView.isVisible = ep.filler
|
||||||
if (ep.filler) View.VISIBLE else View.GONE
|
|
||||||
if (media.userProgress != null) {
|
if (media.userProgress != null) {
|
||||||
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
|
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
|
||||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||||
@@ -248,17 +239,8 @@ class EpisodeAdapter(
|
|||||||
// Find the position of the chapter and notify only that item
|
// Find the position of the chapter and notify only that item
|
||||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
|
|
||||||
media.mainName(),
|
|
||||||
episodeNumber
|
|
||||||
)
|
|
||||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
|
||||||
taskName,
|
|
||||||
""
|
|
||||||
) ?: ""
|
|
||||||
val size = try {
|
val size = try {
|
||||||
val download = index.getDownload(id)
|
bytesToHuman(getDirSize(context, MediaType.ANIME, media.mainName(), episodeNumber))
|
||||||
bytesToHuman(download?.bytesDownloaded ?: 0)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -429,7 +411,7 @@ class EpisodeAdapter(
|
|||||||
if (bytes < 0) return null
|
if (bytes < 0) return null
|
||||||
val unit = 1000
|
val unit = 1000
|
||||||
if (bytes < unit) return "$bytes B"
|
if (bytes < unit) return "$bytes B"
|
||||||
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||||
val pre = ("KMGTPE")[exp - 1]
|
val pre = ("KMGTPE")[exp - 1]
|
||||||
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@ package ani.dantotsu.media.anime
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -12,29 +14,51 @@ import android.util.TypedValue
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||||
|
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
|
import ani.dantotsu.copyToClipboard
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||||
import ani.dantotsu.databinding.ItemStreamBinding
|
import ani.dantotsu.databinding.ItemStreamBinding
|
||||||
import ani.dantotsu.databinding.ItemUrlBinding
|
import ani.dantotsu.databinding.ItemUrlBinding
|
||||||
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.video.Helper
|
import ani.dantotsu.download.video.Helper
|
||||||
|
import ani.dantotsu.hideSystemBars
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.media.SubtitleDownloader
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.others.Download.download
|
import ani.dantotsu.others.Download.download
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
|
import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.parsers.VideoExtractor
|
import ani.dantotsu.parsers.VideoExtractor
|
||||||
import ani.dantotsu.parsers.VideoType
|
import ani.dantotsu.parsers.VideoType
|
||||||
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.toast
|
||||||
|
import ani.dantotsu.tryWith
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
@@ -211,10 +235,101 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val externalPlayerResult = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result: ActivityResult ->
|
||||||
|
Logger.log(result.data.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exportMagnetIntent(episode: Episode, video: Video): Intent {
|
||||||
|
val amnis = "com.amnis"
|
||||||
|
return Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
component = ComponentName(amnis, "$amnis.gui.player.PlayerActivity")
|
||||||
|
data = Uri.parse(video.file.url)
|
||||||
|
putExtra("title", "${media?.name} - ${episode.title}")
|
||||||
|
putExtra("position", 0)
|
||||||
|
putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
putExtra("secure_uri", true)
|
||||||
|
val headersArray = arrayOf<String>()
|
||||||
|
video.file.headers.forEach {
|
||||||
|
headersArray.plus(arrayOf(it.key, it.value))
|
||||||
|
}
|
||||||
|
putExtra("headers", headersArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
fun startExoplayer(media: Media) {
|
fun startExoplayer(media: Media) {
|
||||||
prevEpisode = null
|
prevEpisode = null
|
||||||
|
|
||||||
|
episode?.let { ep ->
|
||||||
|
val video = ep.extractors?.find {
|
||||||
|
it.server.name == ep.selectedExtractor
|
||||||
|
}?.videos?.getOrNull(ep.selectedVideo)
|
||||||
|
video?.file?.url?.let { url ->
|
||||||
|
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
||||||
|
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||||
|
if (torrentExtension.isAvailable()) {
|
||||||
|
val activity = currActivity() ?: requireActivity()
|
||||||
|
launchIO {
|
||||||
|
val extension = torrentExtension.extension!!.extension
|
||||||
|
torrentExtension.torrentHash?.let {
|
||||||
|
extension.removeTorrent(it)
|
||||||
|
}
|
||||||
|
val index = if (url.contains("index=")) {
|
||||||
|
url.substringAfter("index=").toIntOrNull() ?: 0
|
||||||
|
} else 0
|
||||||
|
Logger.log("Sending: ${url}, ${video.quality}, $index")
|
||||||
|
val currentTorrent = extension.addTorrent(
|
||||||
|
url, video.quality.toString(), "", "", false
|
||||||
|
)
|
||||||
|
torrentExtension.torrentHash = currentTorrent.hash
|
||||||
|
video.file.url = extension.getLink(currentTorrent, index)
|
||||||
|
Logger.log("Received: ${video.file.url}")
|
||||||
|
if (launch == true) {
|
||||||
|
Intent(activity, ExoplayerView::class.java).apply {
|
||||||
|
ExoplayerView.media = media
|
||||||
|
ExoplayerView.initialized = true
|
||||||
|
startActivity(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model.setEpisode(
|
||||||
|
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
|
||||||
|
"startExo no launch"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
externalPlayerResult.launch(exportMagnetIntent(ep, video))
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
val amnis = "com.amnis"
|
||||||
|
try {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse("market://details?id=$amnis")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dismiss()
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
||||||
stopAddingToList()
|
stopAddingToList()
|
||||||
@@ -302,7 +417,6 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val video = extractor.videos[position]
|
val video = extractor.videos[position]
|
||||||
@@ -311,6 +425,49 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
} else {
|
} else {
|
||||||
binding.urlDownload.visibility = View.GONE
|
binding.urlDownload.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
val subtitles = extractor.subtitles
|
||||||
|
if (subtitles.isNotEmpty()) {
|
||||||
|
binding.urlSub.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.urlSub.visibility = View.GONE
|
||||||
|
}
|
||||||
|
binding.urlSub.setOnClickListener {
|
||||||
|
if (subtitles.isNotEmpty()) {
|
||||||
|
val subtitleNames = subtitles.map { it.language }
|
||||||
|
var subtitleToDownload: Subtitle? = null
|
||||||
|
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||||
|
.setTitle("Download Subtitle")
|
||||||
|
.setSingleChoiceItems(
|
||||||
|
subtitleNames.toTypedArray(),
|
||||||
|
-1
|
||||||
|
) { _, which ->
|
||||||
|
subtitleToDownload = subtitles[which]
|
||||||
|
}
|
||||||
|
.setPositiveButton("Download") { dialog, _ ->
|
||||||
|
scope.launch {
|
||||||
|
if (subtitleToDownload != null) {
|
||||||
|
SubtitleDownloader.downloadSubtitle(
|
||||||
|
requireContext(),
|
||||||
|
subtitleToDownload!!.file.url,
|
||||||
|
DownloadedType(
|
||||||
|
media!!.mainName(),
|
||||||
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number,
|
||||||
|
MediaType.ANIME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
alertDialog.window?.setDimAmount(0.8f)
|
||||||
|
} else {
|
||||||
|
snackString("No Subtitles Available")
|
||||||
|
}
|
||||||
|
}
|
||||||
binding.urlDownload.setSafeOnClickListener {
|
binding.urlDownload.setSafeOnClickListener {
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||||
extractor.server.name
|
extractor.server.name
|
||||||
@@ -323,20 +480,52 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
media!!.userPreferredName
|
media!!.userPreferredName
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
val downloadAddonManager: DownloadAddonManager = Injekt.get()
|
||||||
|
if (!downloadAddonManager.isAvailable()){
|
||||||
|
toast("Download Extension not available")
|
||||||
|
return@setSafeOnClickListener
|
||||||
|
}
|
||||||
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
||||||
val selectedVideo =
|
val selectedVideo =
|
||||||
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||||
val subtitles = extractor.subtitles
|
|
||||||
val subtitleNames = subtitles.map { it.language }
|
val subtitleNames = subtitles.map { it.language }
|
||||||
var subtitleToDownload: Subtitle? = null
|
var subtitleToDownload: Subtitle? = null
|
||||||
val activity = currActivity()?:requireActivity()
|
val activity = currActivity() ?: requireActivity()
|
||||||
|
selectedVideo?.file?.url?.let { url ->
|
||||||
|
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
||||||
|
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||||
|
if (!torrentExtension.isAvailable()) {
|
||||||
|
toast("Torrent Extension not available")
|
||||||
|
return@setSafeOnClickListener
|
||||||
|
}
|
||||||
|
runBlocking {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val extension = torrentExtension.extension!!.extension
|
||||||
|
torrentExtension.torrentHash?.let {
|
||||||
|
extension.removeTorrent(it)
|
||||||
|
}
|
||||||
|
val index = if (url.contains("index=")) {
|
||||||
|
url.substringAfter("index=").toIntOrNull() ?: 0
|
||||||
|
} else 0
|
||||||
|
Logger.log("Sending: ${url}, ${selectedVideo.quality}, $index")
|
||||||
|
val currentTorrent = extension.addTorrent(
|
||||||
|
url, selectedVideo.quality.toString(), "", "", false
|
||||||
|
)
|
||||||
|
torrentExtension.torrentHash = currentTorrent.hash
|
||||||
|
selectedVideo.file.url =
|
||||||
|
extension.getLink(currentTorrent, index)
|
||||||
|
Logger.log("Received: ${selectedVideo.file.url}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (subtitles.isNotEmpty()) {
|
if (subtitles.isNotEmpty()) {
|
||||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||||
.setTitle("Download Subtitle")
|
.setTitle("Download Subtitle")
|
||||||
.setSingleChoiceItems(
|
.setSingleChoiceItems(
|
||||||
subtitleNames.toTypedArray(),
|
subtitleNames.toTypedArray(),
|
||||||
-1
|
-1
|
||||||
) { dialog, which ->
|
) { _, which ->
|
||||||
subtitleToDownload = subtitles[which]
|
subtitleToDownload = subtitles[which]
|
||||||
}
|
}
|
||||||
.setPositiveButton("Download") { _, _ ->
|
.setPositiveButton("Download") { _, _ ->
|
||||||
@@ -401,12 +590,16 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
if (video.format == VideoType.CONTAINER) {
|
if (video.format == VideoType.CONTAINER) {
|
||||||
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
|
binding.urlSize.isVisible = video.size != null
|
||||||
binding.urlSize.text =
|
// if video size is null or 0, show "Unknown Size" else show the size in MB
|
||||||
// if video size is null or 0, show "Unknown Size" else show the size in MB
|
val sizeText = getString(
|
||||||
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
|
R.string.mb_size, "${if (video.extraNote != null) " : " else ""}${
|
||||||
"#.##"
|
if (video.size == 0.0) getString(R.string.size_unknown) else DecimalFormat("#.##").format(
|
||||||
).format(video.size ?: 0).toString() + " MB"))
|
video.size ?: 0
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
binding.urlSize.text = sizeText
|
||||||
}
|
}
|
||||||
binding.urlNote.visibility = View.VISIBLE
|
binding.urlNote.visibility = View.VISIBLE
|
||||||
binding.urlNote.text = video.format.name
|
binding.urlNote.text = video.format.name
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.graphics.Color.TRANSPARENT
|
import android.graphics.Color.TRANSPARENT
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -68,7 +67,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||||||
binding.subtitleTitle.setText(R.string.none)
|
binding.subtitleTitle.setText(R.string.none)
|
||||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
val mediaID: Int = media.id
|
val mediaID: Int = media.id
|
||||||
val selSubs = PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java)
|
val selSubs = PrefManager.getNullableCustomVal(
|
||||||
|
"subLang_${mediaID}",
|
||||||
|
null,
|
||||||
|
String::class.java
|
||||||
|
)
|
||||||
if (episode.selectedSubtitle != null && selSubs != "None") {
|
if (episode.selectedSubtitle != null && selSubs != "None") {
|
||||||
binding.root.setCardBackgroundColor(TRANSPARENT)
|
binding.root.setCardBackgroundColor(TRANSPARENT)
|
||||||
}
|
}
|
||||||
@@ -108,12 +111,15 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
val mediaID: Int = media.id
|
val mediaID: Int = media.id
|
||||||
val selSubs: String? =
|
val selSubs: String? =
|
||||||
PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java)
|
PrefManager.getNullableCustomVal(
|
||||||
|
"subLang_${mediaID}",
|
||||||
|
null,
|
||||||
|
String::class.java
|
||||||
|
)
|
||||||
if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) {
|
if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) {
|
||||||
binding.root.setCardBackgroundColor(TRANSPARENT)
|
binding.root.setCardBackgroundColor(TRANSPARENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val activity: Activity = requireActivity() as ExoplayerView
|
|
||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
episode.selectedSubtitle = position - 1
|
episode.selectedSubtitle = position - 1
|
||||||
model.setEpisode(episode, "Subtitle")
|
model.setEpisode(episode, "Subtitle")
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||||
|
import androidx.media3.common.C.TrackType
|
||||||
|
import androidx.media3.common.Tracks
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.databinding.BottomSheetSubtitlesBinding
|
||||||
|
import ani.dantotsu.databinding.ItemSubtitleTextBinding
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
class TrackGroupDialogFragment(
|
||||||
|
instance: ExoplayerView, trackGroups: ArrayList<Tracks.Group>, type: @TrackType Int
|
||||||
|
) : BottomSheetDialogFragment() {
|
||||||
|
private var _binding: BottomSheetSubtitlesBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private var instance: ExoplayerView
|
||||||
|
private var trackGroups: ArrayList<Tracks.Group>
|
||||||
|
private var type: @TrackType Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.instance = instance
|
||||||
|
this.trackGroups = trackGroups
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
if (type == TRACK_TYPE_AUDIO) binding.selectionTitle.text = getString(R.string.audio_tracks)
|
||||||
|
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
binding.subtitlesRecycler.adapter = TrackGroupAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class TrackGroupAdapter : RecyclerView.Adapter<TrackGroupAdapter.StreamViewHolder>() {
|
||||||
|
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
||||||
|
StreamViewHolder(
|
||||||
|
ItemSubtitleTextBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||||
|
val binding = holder.binding
|
||||||
|
trackGroups[position].let { trackGroup ->
|
||||||
|
when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) {
|
||||||
|
null -> {
|
||||||
|
binding.subtitleTitle.text =
|
||||||
|
getString(R.string.unknown_track, "Track $position")
|
||||||
|
}
|
||||||
|
|
||||||
|
"none" -> {
|
||||||
|
binding.subtitleTitle.text = getString(R.string.disabled_track)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val locale = if (language.contains("-")) {
|
||||||
|
val parts = language.split("-")
|
||||||
|
try {
|
||||||
|
Locale(parts[0], parts[1])
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Locale(language)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.subtitleTitle.text = locale?.let {
|
||||||
|
"[${it.language}] ${it.displayName}"
|
||||||
|
|
||||||
|
} ?: getString(R.string.unknown_track, language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trackGroup.isSelected) {
|
||||||
|
val selected = "✔ ${binding.subtitleTitle.text}"
|
||||||
|
binding.subtitleTitle.text = selected
|
||||||
|
}
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
instance.onSetTrackGroupOverride(trackGroup, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = trackGroups.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
_binding = null
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.PopupMenu
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
@@ -11,6 +12,7 @@ import ani.dantotsu.connections.comments.Comment
|
|||||||
import ani.dantotsu.connections.comments.CommentsAPI
|
import ani.dantotsu.connections.comments.CommentsAPI
|
||||||
import ani.dantotsu.copyToClipboard
|
import ani.dantotsu.copyToClipboard
|
||||||
import ani.dantotsu.databinding.ItemCommentsBinding
|
import ani.dantotsu.databinding.ItemCommentsBinding
|
||||||
|
import ani.dantotsu.getAppString
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.profile.ProfileActivity
|
import ani.dantotsu.profile.ProfileActivity
|
||||||
@@ -28,17 +30,20 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
class CommentItem(val comment: Comment,
|
class CommentItem(
|
||||||
private val markwon: Markwon,
|
val comment: Comment,
|
||||||
val parentSection: Section,
|
private val markwon: Markwon,
|
||||||
private val commentsFragment: CommentsFragment,
|
val parentSection: Section,
|
||||||
private val backgroundColor: Int,
|
private val commentsFragment: CommentsFragment,
|
||||||
val commentDepth: Int
|
private val backgroundColor: Int,
|
||||||
) : BindableItem<ItemCommentsBinding>() {
|
val commentDepth: Int
|
||||||
|
) :
|
||||||
|
BindableItem<ItemCommentsBinding>() {
|
||||||
lateinit var binding: ItemCommentsBinding
|
lateinit var binding: ItemCommentsBinding
|
||||||
val adapter = GroupieAdapter()
|
val adapter = GroupieAdapter()
|
||||||
private var subCommentIds: MutableList<Int> = mutableListOf()
|
private var subCommentIds: MutableList<Int> = mutableListOf()
|
||||||
@@ -52,18 +57,15 @@ class CommentItem(val comment: Comment,
|
|||||||
adapter.add(repliesSection)
|
adapter.add(repliesSection)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
|
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
|
||||||
binding = viewBinding
|
binding = viewBinding
|
||||||
setAnimation(binding.root.context, binding.root)
|
setAnimation(binding.root.context, binding.root)
|
||||||
viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(commentsFragment.activity)
|
viewBinding.commentRepliesList.layoutManager =
|
||||||
|
LinearLayoutManager(commentsFragment.activity)
|
||||||
viewBinding.commentRepliesList.adapter = adapter
|
viewBinding.commentRepliesList.adapter = adapter
|
||||||
val isUserComment = CommentsAPI.userId == comment.userId
|
val isUserComment = CommentsAPI.userId == comment.userId
|
||||||
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
|
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
|
||||||
markwon.setMarkdown(viewBinding.commentText, comment.content)
|
markwon.setMarkdown(viewBinding.commentText, comment.content)
|
||||||
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
|
|
||||||
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
|
|
||||||
viewBinding.commentReport.visibility = if (!isUserComment) View.VISIBLE else View.GONE
|
|
||||||
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
||||||
if (comment.tag == null) {
|
if (comment.tag == null) {
|
||||||
viewBinding.commentUserTagLayout.visibility = View.GONE
|
viewBinding.commentUserTagLayout.visibility = View.GONE
|
||||||
@@ -76,8 +78,15 @@ class CommentItem(val comment: Comment,
|
|||||||
if ((comment.replyCount ?: 0) > 0) {
|
if ((comment.replyCount ?: 0) > 0) {
|
||||||
viewBinding.commentTotalReplies.visibility = View.VISIBLE
|
viewBinding.commentTotalReplies.visibility = View.VISIBLE
|
||||||
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
|
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
|
||||||
viewBinding.commentTotalReplies.text = if(repliesVisible) "Hide Replies" else
|
viewBinding.commentTotalReplies.context.run {
|
||||||
"View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
|
viewBinding.commentTotalReplies.text = if (repliesVisible)
|
||||||
|
getString(R.string.hide_replies)
|
||||||
|
else
|
||||||
|
if (comment.replyCount == 1)
|
||||||
|
getString(R.string.view_reply)
|
||||||
|
else
|
||||||
|
getString(R.string.view_replies_count, comment.replyCount)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
viewBinding.commentTotalReplies.visibility = View.GONE
|
viewBinding.commentTotalReplies.visibility = View.GONE
|
||||||
viewBinding.commentRepliesDivider.visibility = View.GONE
|
viewBinding.commentRepliesDivider.visibility = View.GONE
|
||||||
@@ -87,10 +96,15 @@ class CommentItem(val comment: Comment,
|
|||||||
if (repliesVisible) {
|
if (repliesVisible) {
|
||||||
repliesSection.clear()
|
repliesSection.clear()
|
||||||
removeSubCommentIds()
|
removeSubCommentIds()
|
||||||
viewBinding.commentTotalReplies.text = "View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
|
viewBinding.commentTotalReplies.context.run {
|
||||||
|
viewBinding.commentTotalReplies.text = if (comment.replyCount == 1)
|
||||||
|
getString(R.string.view_reply)
|
||||||
|
else
|
||||||
|
getString(R.string.view_replies_count, comment.replyCount)
|
||||||
|
}
|
||||||
repliesVisible = false
|
repliesVisible = false
|
||||||
} else {
|
} else {
|
||||||
viewBinding.commentTotalReplies.text = "Hide Replies"
|
viewBinding.commentTotalReplies.setText(R.string.hide_replies)
|
||||||
repliesSection.clear()
|
repliesSection.clear()
|
||||||
commentsFragment.viewReplyCallback(this)
|
commentsFragment.viewReplyCallback(this)
|
||||||
repliesVisible = true
|
repliesVisible = true
|
||||||
@@ -99,16 +113,20 @@ class CommentItem(val comment: Comment,
|
|||||||
|
|
||||||
viewBinding.commentUserName.setOnClickListener {
|
viewBinding.commentUserName.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
|
commentsFragment.activity,
|
||||||
|
Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||||
.putExtra("userId", comment.userId.toInt())
|
.putExtra("userId", comment.userId.toInt())
|
||||||
.putExtra("userLVL","[${levelColor.second}]"), null
|
.putExtra("userLVL", "[${levelColor.second}]"),
|
||||||
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewBinding.commentUserAvatar.setOnClickListener {
|
viewBinding.commentUserAvatar.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
|
commentsFragment.activity,
|
||||||
|
Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||||
.putExtra("userId", comment.userId.toInt())
|
.putExtra("userId", comment.userId.toInt())
|
||||||
.putExtra("userLVL","[${levelColor.second}]"), null
|
.putExtra("userLVL", "[${levelColor.second}]"),
|
||||||
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewBinding.commentText.setOnLongClickListener {
|
viewBinding.commentText.setOnLongClickListener {
|
||||||
@@ -127,39 +145,73 @@ class CommentItem(val comment: Comment,
|
|||||||
}
|
}
|
||||||
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
|
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
|
||||||
viewBinding.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
|
viewBinding.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
|
||||||
viewBinding.commentDelete.setOnClickListener {
|
viewBinding.commentInfo.setOnClickListener {
|
||||||
dialogBuilder("Delete Comment", "Are you sure you want to delete this comment?") {
|
val popup = PopupMenu(commentsFragment.requireContext(), viewBinding.commentInfo)
|
||||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
popup.menuInflater.inflate(R.menu.profile_details_menu, popup.menu)
|
||||||
scope.launch {
|
popup.menu.findItem(R.id.commentDelete)?.isVisible =
|
||||||
val success = CommentsAPI.deleteComment(comment.commentId)
|
isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod
|
||||||
if (success) {
|
popup.menu.findItem(R.id.commentBanUser)?.isVisible =
|
||||||
snackString("Comment Deleted")
|
(CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment
|
||||||
parentSection.remove(this@CommentItem)
|
popup.menu.findItem(R.id.commentReport)?.isVisible = !isUserComment
|
||||||
}
|
popup.setOnMenuItemClickListener { item ->
|
||||||
}
|
when (item.itemId) {
|
||||||
}
|
R.id.commentReport -> {
|
||||||
}
|
dialogBuilder(
|
||||||
viewBinding.commentBanUser.setOnClickListener {
|
getAppString(R.string.report_comment),
|
||||||
dialogBuilder("Ban User", "Are you sure you want to ban this user?") {
|
getAppString(R.string.report_comment_confirm)
|
||||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
) {
|
||||||
scope.launch {
|
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||||
val success = CommentsAPI.banUser(comment.userId)
|
val success = CommentsAPI.reportComment(
|
||||||
if (success) {
|
comment.commentId,
|
||||||
snackString("User Banned")
|
comment.username,
|
||||||
}
|
commentsFragment.mediaName,
|
||||||
}
|
comment.userId
|
||||||
}
|
)
|
||||||
}
|
if (success) {
|
||||||
viewBinding.commentReport.setOnClickListener {
|
snackString(R.string.comment_reported)
|
||||||
dialogBuilder("Report Comment", "Only report comments that violate the rules. Are you sure you want to report this comment?") {
|
}
|
||||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
}
|
||||||
scope.launch {
|
}
|
||||||
val success = CommentsAPI.reportComment(comment.commentId, comment.username, commentsFragment.mediaName, comment.userId)
|
true
|
||||||
if (success) {
|
}
|
||||||
snackString("Comment Reported")
|
|
||||||
|
R.id.commentDelete -> {
|
||||||
|
dialogBuilder(
|
||||||
|
getAppString(R.string.delete_comment),
|
||||||
|
getAppString(R.string.delete_comment_confirm)
|
||||||
|
) {
|
||||||
|
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||||
|
val success = CommentsAPI.deleteComment(comment.commentId)
|
||||||
|
if (success) {
|
||||||
|
snackString(R.string.comment_deleted)
|
||||||
|
parentSection.remove(this@CommentItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.commentBanUser -> {
|
||||||
|
dialogBuilder(
|
||||||
|
getAppString(R.string.ban_user),
|
||||||
|
getAppString(R.string.ban_user_confirm)
|
||||||
|
) {
|
||||||
|
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||||
|
val success = CommentsAPI.banUser(comment.userId)
|
||||||
|
if (success) {
|
||||||
|
snackString(R.string.user_banned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
popup.show()
|
||||||
}
|
}
|
||||||
//fill the icon if the user has liked the comment
|
//fill the icon if the user has liked the comment
|
||||||
setVoteButtons(viewBinding)
|
setVoteButtons(viewBinding)
|
||||||
@@ -195,7 +247,6 @@ class CommentItem(val comment: Comment,
|
|||||||
comment.upvotes -= 1
|
comment.upvotes -= 1
|
||||||
}
|
}
|
||||||
comment.downvotes += if (voteType == -1) 1 else -1
|
comment.downvotes += if (voteType == -1) 1 else -1
|
||||||
|
|
||||||
notifyChanged()
|
notifyChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,7 +261,8 @@ class CommentItem(val comment: Comment,
|
|||||||
}
|
}
|
||||||
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
|
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
|
||||||
viewBinding.commentUserName.text = comment.username
|
viewBinding.commentUserName.text = comment.username
|
||||||
viewBinding.commentUserLevel.text = "[${levelColor.second}]"
|
val userColor = "[${levelColor.second}]"
|
||||||
|
viewBinding.commentUserLevel.text = userColor
|
||||||
viewBinding.commentUserLevel.setTextColor(levelColor.first)
|
viewBinding.commentUserLevel.setTextColor(levelColor.first)
|
||||||
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
|
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
|
||||||
}
|
}
|
||||||
@@ -228,12 +280,16 @@ class CommentItem(val comment: Comment,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun replying(isReplying: Boolean) {
|
fun replying(isReplying: Boolean) {
|
||||||
binding.commentReply.text = if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
|
binding.commentReply.text =
|
||||||
|
if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
|
||||||
this.isReplying = isReplying
|
this.isReplying = isReplying
|
||||||
}
|
}
|
||||||
|
|
||||||
fun editing(isEditing: Boolean) {
|
fun editing(isEditing: Boolean) {
|
||||||
binding.commentEdit.text = if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(R.string.edit)
|
binding.commentEdit.text =
|
||||||
|
if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(
|
||||||
|
R.string.edit
|
||||||
|
)
|
||||||
this.isEditing = isEditing
|
this.isEditing = isEditing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,8 +297,9 @@ class CommentItem(val comment: Comment,
|
|||||||
subCommentIds.add(id)
|
subCommentIds.add(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeSubCommentIds(){
|
private fun removeSubCommentIds() {
|
||||||
subCommentIds.forEach { id ->
|
subCommentIds.forEach { id ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
|
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
|
||||||
val commentToRemove = parentComments.find { it.comment.commentId == id }
|
val commentToRemove = parentComments.find { it.comment.commentId == id }
|
||||||
commentToRemove?.let {
|
commentToRemove?.let {
|
||||||
@@ -260,11 +317,13 @@ class CommentItem(val comment: Comment,
|
|||||||
viewBinding.commentUpVote.alpha = 1f
|
viewBinding.commentUpVote.alpha = 1f
|
||||||
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||||
}
|
}
|
||||||
|
|
||||||
-1 -> {
|
-1 -> {
|
||||||
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||||
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
||||||
viewBinding.commentDownVote.alpha = 1f
|
viewBinding.commentDownVote.alpha = 1f
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||||
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||||
@@ -272,9 +331,10 @@ class CommentItem(val comment: Comment,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
private fun formatTimestamp(timestamp: String): String {
|
private fun formatTimestamp(timestamp: String): String {
|
||||||
return try {
|
return try {
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
val parsedDate = dateFormat.parse(timestamp)
|
val parsedDate = dateFormat.parse(timestamp)
|
||||||
val currentDate = Date()
|
val currentDate = Date()
|
||||||
@@ -297,8 +357,9 @@ class CommentItem(val comment: Comment,
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
fun timestampToMillis(timestamp: String): Long {
|
fun timestampToMillis(timestamp: String): Long {
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
val parsedDate = dateFormat.parse(timestamp)
|
val parsedDate = dateFormat.parse(timestamp)
|
||||||
return parsedDate?.time ?: 0
|
return parsedDate?.time ?: 0
|
||||||
@@ -307,7 +368,8 @@ class CommentItem(val comment: Comment,
|
|||||||
|
|
||||||
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair<Int, Int> {
|
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair<Int, Int> {
|
||||||
val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt()
|
val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt()
|
||||||
val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
|
val colorString =
|
||||||
|
if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
|
||||||
var color = Color.parseColor(colorString)
|
var color = Color.parseColor(colorString)
|
||||||
val ratio = getContrastRatio(color, backgroundColor)
|
val ratio = getContrastRatio(color, backgroundColor)
|
||||||
if (ratio < 4.5) {
|
if (ratio < 4.5) {
|
||||||
@@ -325,16 +387,17 @@ class CommentItem(val comment: Comment,
|
|||||||
* @param callback the callback to call when the user clicks yes
|
* @param callback the callback to call when the user clicks yes
|
||||||
*/
|
*/
|
||||||
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
|
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
|
||||||
val alertDialog = android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
|
val alertDialog =
|
||||||
.setTitle(title)
|
android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
|
||||||
.setMessage(message)
|
.setTitle(title)
|
||||||
.setPositiveButton("Yes") { dialog, _ ->
|
.setMessage(message)
|
||||||
callback()
|
.setPositiveButton("Yes") { dialog, _ ->
|
||||||
dialog.dismiss()
|
callback()
|
||||||
}
|
dialog.dismiss()
|
||||||
.setNegativeButton("No") { dialog, _ ->
|
}
|
||||||
dialog.dismiss()
|
.setNegativeButton("No") { dialog, _ ->
|
||||||
}
|
dialog.dismiss()
|
||||||
|
}
|
||||||
val dialog = alertDialog.show()
|
val dialog = alertDialog.show()
|
||||||
dialog?.window?.setDimAmount(0.8f)
|
dialog?.window?.setDimAmount(0.8f)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user