mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-23 06:11:02 +00:00
Compare commits
2 Commits
l10n_dev_c
...
v2.2.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d778cd4350 | ||
|
|
ab199a3502 |
36
.github/workflows/beta.yml
vendored
36
.github/workflows/beta.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "Commits since $LAST_SHA:"
|
echo "Commits since $LAST_SHA:"
|
||||||
# Accumulate commit logs in a shell variable
|
# Accumulate commit logs in a shell variable
|
||||||
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an")
|
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"%h - %s")
|
||||||
# URL-encode the newline characters for GitHub Actions
|
# URL-encode the newline characters for GitHub Actions
|
||||||
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
|
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
|
||||||
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
|
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
|
||||||
@@ -48,11 +48,9 @@ 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
|
||||||
@@ -66,7 +64,7 @@ jobs:
|
|||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup JDK 17
|
- name: Setup JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: 17
|
java-version: 17
|
||||||
@@ -77,7 +75,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
|
||||||
|
|
||||||
@@ -85,31 +83,28 @@ 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
|
uses: actions/upload-artifact@v3.0.0
|
||||||
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
|
||||||
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
#Discord
|
#Discord
|
||||||
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
|
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g')
|
||||||
# Truncate commit messages if they are too long
|
# Truncate commit messages if they are too long
|
||||||
max_length=1900 # Adjust this value as needed
|
max_length=1900 # Adjust this value as needed
|
||||||
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: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
#Telegram
|
#Telegram
|
||||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
|
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
|
||||||
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
|
-F "caption=[Alpha-Build: ${VERSION}] Change logs :${commit_messages}" \
|
||||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -117,13 +112,18 @@ jobs:
|
|||||||
VERSION: ${{ env.VERSION }}
|
VERSION: ${{ env.VERSION }}
|
||||||
|
|
||||||
- name: Upload Current SHA as Artifact
|
- name: Upload Current SHA as Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: last-sha
|
name: last-sha
|
||||||
path: last_sha.txt
|
path: last_sha.txt
|
||||||
|
|
||||||
- name: Upload Commit log as Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
- name: Delete Old Pre-Releases
|
||||||
|
id: delete-pre-releases
|
||||||
|
uses: sgpublic/delete-release-action@master
|
||||||
with:
|
with:
|
||||||
name: commit-log
|
pre-release-drop: true
|
||||||
path: commit_log.txt
|
pre-release-keep-count: 3
|
||||||
|
pre-release-drop-tag: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,9 +8,6 @@ local.properties
|
|||||||
# Log/OS Files
|
# Log/OS Files
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Secrets
|
|
||||||
apikey.properties
|
|
||||||
|
|
||||||
# Android Studio generated files and folders
|
# Android Studio generated files and folders
|
||||||
captures/
|
captures/
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
@@ -31,6 +28,3 @@ output.json
|
|||||||
|
|
||||||
#other
|
#other
|
||||||
scripts/
|
scripts/
|
||||||
|
|
||||||
#crowdin
|
|
||||||
crowdin.yml
|
|
||||||
@@ -15,16 +15,15 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "ani.dantotsu"
|
applicationId "ani.dantotsu"
|
||||||
minSdk 21
|
minSdk 23
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||||
versionName "3.0.0"
|
versionName "2.2.0"
|
||||||
versionCode 300000000
|
versionCode 220000000
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions += "store"
|
flavorDimensions "store"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
fdroid {
|
fdroid {
|
||||||
// F-Droid specific configuration
|
// F-Droid specific configuration
|
||||||
@@ -43,22 +42,19 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
alpha {
|
alpha {
|
||||||
applicationIdSuffix ".beta" // keep as beta by popular request
|
applicationIdSuffix ".beta" // keep as beta by popular request
|
||||||
versionNameSuffix "-alpha01-" + gitCommitHash
|
versionNameSuffix "-alpha01"
|
||||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
|
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", 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
|
||||||
isDefault true
|
isDefault true
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix ".beta"
|
applicationIdSuffix ".beta"
|
||||||
versionNameSuffix "-beta02"
|
versionNameSuffix "-beta01"
|
||||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
|
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
|
||||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
|
|
||||||
debuggable false
|
debuggable false
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher"
|
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
|
||||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_round"
|
|
||||||
debuggable false
|
debuggable false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
@@ -80,13 +76,13 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// FireBase
|
// FireBase
|
||||||
googleImplementation platform('com.google.firebase:firebase-bom:32.8.1')
|
googleImplementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.6.2'
|
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
|
||||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.4'
|
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.1'
|
||||||
// Core
|
// Core
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.browser:browser:1.8.0'
|
implementation 'androidx.browser:browser:1.7.0'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
@@ -95,14 +91,13 @@ dependencies {
|
|||||||
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'com.google.code.gson:gson:2.10.1'
|
implementation 'com.google.code.gson:gson:2.10'
|
||||||
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
|
||||||
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"
|
||||||
@@ -110,48 +105,33 @@ 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.1'
|
ext.exo_version = '1.2.1'
|
||||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||||
implementation "androidx.media3:media3-ui:$exo_version"
|
implementation "androidx.media3:media3-ui:$exo_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||||
implementation "androidx.media3:media3-session:$exo_version"
|
implementation "androidx.media3:media3-session:$exo_version"
|
||||||
// Media3 Casting
|
//media3 casting
|
||||||
implementation "androidx.media3:media3-cast:$exo_version"
|
implementation "androidx.media3:media3-cast:$exo_version"
|
||||||
implementation "androidx.mediarouter:mediarouter:1.7.0"
|
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'com.github.RepoDevil:AnimatedBottomBar:7fcb9af'
|
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||||
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
implementation '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:7.2.1'
|
|
||||||
|
|
||||||
// Markwon
|
// string matching
|
||||||
ext.markwon_version = '4.6.2'
|
|
||||||
implementation "io.noties.markwon:core:$markwon_version"
|
|
||||||
implementation "io.noties.markwon:editor:$markwon_version"
|
|
||||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
|
||||||
implementation "io.noties.markwon:ext-tables:$markwon_version"
|
|
||||||
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
|
|
||||||
implementation "io.noties.markwon:html:$markwon_version"
|
|
||||||
implementation "io.noties.markwon:image-glide:$markwon_version"
|
|
||||||
|
|
||||||
// Groupie
|
|
||||||
ext.groupie_version = '2.10.1'
|
|
||||||
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
|
|
||||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
|
|
||||||
|
|
||||||
// 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'
|
||||||
@@ -161,10 +141,11 @@ dependencies {
|
|||||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
|
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||||
implementation 'com.squareup.okio:okio:3.8.0'
|
implementation 'com.squareup.okio:okio:3.7.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
|
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
|
||||||
implementation 'org.jsoup:jsoup:1.16.1'
|
implementation 'ch.acra:acra-http:5.11.3'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
|
implementation 'org.jsoup:jsoup:1.15.4'
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
|
||||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
|
|||||||
19
app/proguard-rules.pro
vendored
19
app/proguard-rules.pro
vendored
@@ -43,25 +43,6 @@
|
|||||||
public static <1> INSTANCE;
|
public static <1> INSTANCE;
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
-keep class ani.dantotsu.** { *; }
|
|
||||||
-keep class ani.dantotsu.download.DownloadsManager { *; }
|
|
||||||
-keepattributes Signature
|
|
||||||
-keep class uy.kohesive.injekt.** { *; }
|
|
||||||
-keep class eu.kanade.tachiyomi.** { *; }
|
|
||||||
-keep class kotlin.** { *; }
|
|
||||||
-dontwarn kotlin.**
|
|
||||||
-keep class kotlinx.** { *; }
|
|
||||||
-keepclassmembers class uy.kohesive.injekt.api.FullTypeReference {
|
|
||||||
<init>(...);
|
|
||||||
}
|
|
||||||
-keep class com.google.gson.** { *; }
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
-keepattributes EnclosingMethod
|
|
||||||
-keep class com.google.gson.reflect.TypeToken { *; }
|
|
||||||
-keep class org.jsoup.** { *; }
|
|
||||||
-keepclassmembers class org.jsoup.nodes.Document { *; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,13 @@ import android.net.Uri
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import ani.dantotsu.BuildConfig
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.Mapper
|
|
||||||
import ani.dantotsu.R
|
|
||||||
import ani.dantotsu.buildMarkwon
|
|
||||||
import ani.dantotsu.client
|
|
||||||
import ani.dantotsu.currContext
|
|
||||||
import ani.dantotsu.logError
|
|
||||||
import ani.dantotsu.openLinkInBrowser
|
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.snackString
|
import io.noties.markwon.Markwon
|
||||||
import ani.dantotsu.toast
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
import ani.dantotsu.tryWithSuspend
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -33,8 +25,9 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.*
|
||||||
|
|
||||||
object AppUpdater {
|
object AppUpdater {
|
||||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||||
@@ -46,10 +39,9 @@ object AppUpdater {
|
|||||||
.parsed<JsonArray>().map {
|
.parsed<JsonArray>().map {
|
||||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||||
}
|
}
|
||||||
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }
|
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }.maxByOrNull {
|
||||||
.maxByOrNull {
|
it.timeStamp()
|
||||||
it.timeStamp()
|
} ?: throw Exception("No Pre Release Found")
|
||||||
} ?: throw Exception("No Pre Release Found")
|
|
||||||
val v = r.tagName.substringAfter("v", "")
|
val v = r.tagName.substringAfter("v", "")
|
||||||
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||||
} else {
|
} else {
|
||||||
@@ -58,7 +50,7 @@ object AppUpdater {
|
|||||||
res to res.substringAfter("# ").substringBefore("\n")
|
res to res.substringAfter("# ").substringBefore("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.log("Git Version : $version")
|
logger("Git Version : $version")
|
||||||
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
|
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
|
||||||
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
|
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
|
||||||
CustomBottomDialog.newInstance().apply {
|
CustomBottomDialog.newInstance().apply {
|
||||||
@@ -69,7 +61,8 @@ object AppUpdater {
|
|||||||
)
|
)
|
||||||
addView(
|
addView(
|
||||||
TextView(activity).apply {
|
TextView(activity).apply {
|
||||||
val markWon = buildMarkwon(activity, false)
|
val markWon = Markwon.builder(activity)
|
||||||
|
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||||
markWon.setMarkdown(this, md)
|
markWon.setMarkdown(this, md)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -85,18 +78,13 @@ 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 {
|
||||||
val apks =
|
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
.parsed<GithubResponse>().assets?.find {
|
||||||
.parsed<GithubResponse>().assets?.filter {
|
it.browserDownloadURL.endsWith("apk")
|
||||||
it.browserDownloadURL.endsWith(
|
}?.browserDownloadURL.apply {
|
||||||
".apk"
|
if (this != null) activity.downloadUpdate(version, this)
|
||||||
)
|
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)
|
||||||
}
|
}
|
||||||
@@ -116,25 +104,24 @@ 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
val new = toDouble(version.split("."))
|
if (BuildConfig.DEBUG) {
|
||||||
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
return BuildConfig.VERSION_NAME != version
|
||||||
new > curr
|
} 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val new = toDouble(version.split("."))
|
||||||
|
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
||||||
|
return new > curr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,8 +161,21 @@ object AppUpdater {
|
|||||||
DownloadManager.EXTRA_DOWNLOAD_ID, id
|
DownloadManager.EXTRA_DOWNLOAD_ID, id
|
||||||
) ?: id
|
) ?: id
|
||||||
|
|
||||||
downloadManager.getUriForDownloadedFile(downloadId)?.let {
|
val query = DownloadManager.Query()
|
||||||
openApk(this@downloadUpdate, it)
|
query.setFilterById(downloadId)
|
||||||
|
val c = downloadManager.query(query)
|
||||||
|
|
||||||
|
if (c.moveToFirst()) {
|
||||||
|
val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||||
|
if (DownloadManager.STATUS_SUCCESSFUL == c
|
||||||
|
.getInt(columnIndex)
|
||||||
|
) {
|
||||||
|
c.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI)
|
||||||
|
val uri = Uri.parse(
|
||||||
|
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||||
|
)
|
||||||
|
openApk(this@downloadUpdate, uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
@@ -190,11 +190,16 @@ object AppUpdater {
|
|||||||
private fun openApk(context: Context, uri: Uri) {
|
private fun openApk(context: Context, uri: Uri) {
|
||||||
try {
|
try {
|
||||||
uri.path?.let {
|
uri.path?.let {
|
||||||
|
val contentUri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".provider",
|
||||||
|
File(it)
|
||||||
|
)
|
||||||
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
|
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
|
||||||
data = uri
|
data = contentUri
|
||||||
}
|
}
|
||||||
context.startActivity(installIntent)
|
context.startActivity(installIntent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
<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" />
|
||||||
@@ -18,10 +16,10 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29" />
|
android:maxSdkVersion="32" />
|
||||||
<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 -->
|
||||||
@@ -40,17 +38,6 @@
|
|||||||
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" />
|
||||||
@@ -62,7 +49,6 @@
|
|||||||
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"
|
||||||
@@ -71,30 +57,9 @@
|
|||||||
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.upcoming.UpcomingWidget"
|
android:name=".widgets.CurrentlyAiringWidget"
|
||||||
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" />
|
||||||
@@ -102,9 +67,10 @@
|
|||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/statistics_widget_info" />
|
android:resource="@xml/currently_airing_widget_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
|
<receiver android:name=".subcriptions.NotificationClickReceiver" />
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".media.novel.novelreader.NovelReaderActivity"
|
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||||
@@ -136,61 +102,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
<activity
|
|
||||||
android:name=".settings.SettingsAboutActivity"
|
|
||||||
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
|
<activity
|
||||||
android:name=".settings.ExtensionsActivity"
|
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
|
|
||||||
android:name=".profile.ProfileActivity"
|
|
||||||
android:parentActivityName=".MainActivity"
|
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
|
||||||
<activity
|
|
||||||
android:name=".profile.FollowActivity"
|
|
||||||
android:parentActivityName=".MainActivity"
|
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
|
||||||
<activity
|
|
||||||
android:name=".profile.activity.FeedActivity"
|
|
||||||
android:configChanges="orientation|screenSize|screenLayout"
|
|
||||||
android:label="Inbox Activity"
|
|
||||||
android:parentActivityName=".MainActivity" />
|
|
||||||
<activity
|
|
||||||
android:name=".profile.activity.NotificationActivity"
|
|
||||||
android:label="Inbox Activity"
|
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".others.imagesearch.ImageSearchActivity"
|
android:name=".others.imagesearch.ImageSearchActivity"
|
||||||
@@ -204,9 +117,6 @@
|
|||||||
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"
|
|
||||||
android:parentActivityName=".profile.ProfileActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".media.manga.mangareader.MangaReaderActivity"
|
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
@@ -217,8 +127,7 @@
|
|||||||
<activity
|
<activity
|
||||||
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" />
|
|
||||||
<activity android:name=".media.CharacterDetailsActivity" />
|
<activity android:name=".media.CharacterDetailsActivity" />
|
||||||
<activity android:name=".home.NoInternet" />
|
<activity android:name=".home.NoInternet" />
|
||||||
<activity
|
<activity
|
||||||
@@ -330,17 +239,6 @@
|
|||||||
<data android:host="myanimelist.net" />
|
<data android:host="myanimelist.net" />
|
||||||
<data android:pathPrefix="/anime" />
|
<data android:pathPrefix="/anime" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:label="@string/view_profile_in_dantotsu">
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="http" />
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:host="anilist.co" />
|
|
||||||
<data android:pathPrefix="/user" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@@ -356,40 +254,24 @@
|
|||||||
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
|
|
||||||
<data android:scheme="content" />
|
|
||||||
<data android:mimeType="*/*" />
|
|
||||||
<data android:pathPattern=".*\\.ani" />
|
|
||||||
<data android:pathPattern=".*\\.sani" />
|
|
||||||
<data android:host="*" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||||
|
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
|
<receiver
|
||||||
android:name=".notifications.AlarmPermissionStateReceiver"
|
android:name=".subcriptions.AlarmReceiver"
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver
|
|
||||||
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" />
|
||||||
|
<action android:name="Aani.dantotsu.ACTION_ALARM" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver" />
|
|
||||||
<receiver android:name=".notifications.comment.CommentNotificationReceiver" />
|
|
||||||
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver" />
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="preloaded_fonts"
|
android:name="preloaded_fonts"
|
||||||
@@ -407,11 +289,25 @@
|
|||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
|
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
<service
|
<service
|
||||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
android:name=".download.video.ExoplayerDownloadService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</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
|
||||||
@@ -434,11 +330,6 @@
|
|||||||
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,13 +6,9 @@ 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.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.notifications.TaskScheduler
|
|
||||||
import ani.dantotsu.others.DisabledReports
|
import ani.dantotsu.others.DisabledReports
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.parsers.MangaSources
|
||||||
@@ -21,8 +17,6 @@ import ani.dantotsu.parsers.novel.NovelExtensionManager
|
|||||||
import ani.dantotsu.settings.SettingsActivity
|
import ani.dantotsu.settings.SettingsActivity
|
||||||
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.util.FinalExceptionHandler
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
@@ -34,6 +28,7 @@ import kotlinx.coroutines.launch
|
|||||||
import logcat.AndroidLogcatLogger
|
import logcat.AndroidLogcatLogger
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.LogcatLogger
|
import logcat.LogcatLogger
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@@ -43,9 +38,6 @@ 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)
|
||||||
@@ -87,11 +79,9 @@ class App : MultiDexApplication() {
|
|||||||
}
|
}
|
||||||
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
||||||
|
|
||||||
Logger.init(this)
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
|
||||||
Logger.log("App: Logging started")
|
|
||||||
|
|
||||||
initializeNetwork()
|
|
||||||
|
initializeNetwork(baseContext)
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
if (!LogcatLogger.isInstalled) {
|
if (!LogcatLogger.isInstalled) {
|
||||||
@@ -101,49 +91,33 @@ 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 {
|
||||||
animeExtensionManager.findAvailableExtensions()
|
animeExtensionManager.findAvailableExtensions()
|
||||||
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||||
}
|
}
|
||||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||||
mangaScope.launch {
|
mangaScope.launch {
|
||||||
mangaExtensionManager.findAvailableExtensions()
|
mangaExtensionManager.findAvailableExtensions()
|
||||||
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||||
}
|
}
|
||||||
val novelScope = CoroutineScope(Dispatchers.Default)
|
val novelScope = CoroutineScope(Dispatchers.Default)
|
||||||
novelScope.launch {
|
novelScope.launch {
|
||||||
novelExtensionManager.findAvailableExtensions()
|
novelExtensionManager.findAvailableExtensions()
|
||||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
logger("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)
|
|
||||||
commentsScope.launch {
|
|
||||||
CommentsAPI.fetchAuthToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
|
|
||||||
val scheduler = TaskScheduler.create(this, useAlarmManager)
|
|
||||||
scheduler.scheduleAllTasks(this)
|
|
||||||
scheduler.scheduleSingleWork(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun setupNotificationChannels() {
|
private fun setupNotificationChannels() {
|
||||||
try {
|
try {
|
||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Failed to modify notification channels")
|
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
||||||
Logger.log(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,10 +140,6 @@ 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
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
package ani.dantotsu
|
package ani.dantotsu
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.DatePickerDialog
|
import android.app.DatePickerDialog
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
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
|
||||||
@@ -19,68 +16,32 @@ import android.content.res.Configuration
|
|||||||
import android.content.res.Resources.getSystem
|
import android.content.res.Resources.getSystem
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Drawable
|
import android.Manifest
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
|
import android.net.NetworkCapabilities.*
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
|
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
|
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_LOWPAN
|
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_USB
|
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_VPN
|
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.*
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.CountDownTimer
|
|
||||||
import android.os.Environment
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.PowerManager
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.GestureDetector
|
import android.view.*
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewAnimationUtils
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.*
|
||||||
import android.view.animation.AlphaAnimation
|
import android.widget.*
|
||||||
import android.view.animation.Animation
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.view.animation.AnimationSet
|
|
||||||
import android.view.animation.OvershootInterpolator
|
|
||||||
import android.view.animation.ScaleAnimation
|
|
||||||
import android.view.animation.TranslateAnimation
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.AutoCompleteTextView
|
|
||||||
import android.widget.DatePicker
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
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.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
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.*
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -88,31 +49,18 @@ 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
|
||||||
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
|
|
||||||
import ani.dantotsu.others.SpoilerPlugin
|
|
||||||
import ani.dantotsu.parsers.ShowResponse
|
import ani.dantotsu.parsers.ShowResponse
|
||||||
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.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.subcriptions.NotificationClickReceiver
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestBuilder
|
|
||||||
import com.bumptech.glide.RequestManager
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
||||||
import com.bumptech.glide.load.resource.gif.GifDrawable
|
|
||||||
import com.bumptech.glide.request.RequestListener
|
|
||||||
import com.bumptech.glide.request.RequestOptions
|
|
||||||
import com.bumptech.glide.request.target.Target
|
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
@@ -120,40 +68,15 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|||||||
import com.google.android.material.internal.ViewUtils
|
import com.google.android.material.internal.ViewUtils
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin
|
import kotlinx.coroutines.*
|
||||||
import io.noties.markwon.Markwon
|
|
||||||
import io.noties.markwon.MarkwonConfiguration
|
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
|
||||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
|
|
||||||
import io.noties.markwon.ext.tables.TablePlugin
|
|
||||||
import io.noties.markwon.ext.tasklist.TaskListPlugin
|
|
||||||
import io.noties.markwon.html.HtmlPlugin
|
|
||||||
import io.noties.markwon.html.TagHandlerNoOp
|
|
||||||
import io.noties.markwon.image.AsyncDrawable
|
|
||||||
import io.noties.markwon.image.glide.GlideImagesPlugin
|
|
||||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.MainScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
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
|
||||||
import java.io.File
|
import java.io.*
|
||||||
import java.io.FileOutputStream
|
import java.lang.Runnable
|
||||||
import java.io.OutputStream
|
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.util.Calendar
|
import java.util.*
|
||||||
import java.util.TimeZone
|
import kotlin.math.*
|
||||||
import java.util.Timer
|
|
||||||
import java.util.TimerTask
|
|
||||||
import kotlin.collections.set
|
|
||||||
import kotlin.math.log2
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
|
|
||||||
var statusBarHeight = 0
|
var statusBarHeight = 0
|
||||||
@@ -185,10 +108,11 @@ fun currActivity(): Activity? {
|
|||||||
var loadMedia: Int? = null
|
var loadMedia: Int? = null
|
||||||
var loadIsMAL = false
|
var loadIsMAL = false
|
||||||
|
|
||||||
val Int.toPx
|
fun logger(e: Any?, print: Boolean = true) {
|
||||||
get() = TypedValue.applyDimension(
|
if (print)
|
||||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
|
println(e)
|
||||||
).toInt()
|
}
|
||||||
|
|
||||||
|
|
||||||
fun initActivity(a: Activity) {
|
fun initActivity(a: Activity) {
|
||||||
val window = a.window
|
val window = a.window
|
||||||
@@ -208,17 +132,11 @@ fun initActivity(a: Activity) {
|
|||||||
if (navBarHeight == 0) {
|
if (navBarHeight == 0) {
|
||||||
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.systemBars()).bottom
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowInsetsControllerCompat(
|
a.hideStatusBar()
|
||||||
window,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
window.decorView
|
|
||||||
).hide(WindowInsetsCompat.Type.statusBars())
|
|
||||||
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())
|
||||||
@@ -230,124 +148,47 @@ fun initActivity(a: Activity) {
|
|||||||
val windowInsets =
|
val windowInsets =
|
||||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||||
if (windowInsets != null) {
|
if (windowInsets != null) {
|
||||||
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
navBarHeight =
|
statusBarHeight = insets.top
|
||||||
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
navBarHeight = insets.bottom
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (a !is MainActivity) a.setNavigationTheme()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
fun Activity.hideSystemBars() {
|
fun Activity.hideSystemBars() {
|
||||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
window.decorView.systemUiVisibility = (
|
||||||
controller.systemBarsBehavior =
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
}
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.hideSystemBarsExtendView() {
|
@Suppress("DEPRECATION")
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
fun Activity.hideStatusBar() {
|
||||||
hideSystemBars()
|
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.showSystemBars() {
|
|
||||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
|
||||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
|
||||||
controller.show(WindowInsetsCompat.Type.systemBars())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.showSystemBarsRetractView() {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
|
||||||
showSystemBars()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.setNavigationTheme() {
|
|
||||||
val tv = TypedValue()
|
|
||||||
theme.resolveAttribute(android.R.attr.colorBackground, tv, true)
|
|
||||||
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && tv.isColorType)
|
|
||||||
|| (tv.type >= TypedValue.TYPE_FIRST_COLOR_INT && tv.type <= TypedValue.TYPE_LAST_COLOR_INT)
|
|
||||||
) {
|
|
||||||
window.navigationBarColor = tv.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()
|
||||||
dialog?.window?.let { window ->
|
val window = dialog?.window
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
val decorView: View = window?.decorView ?: return
|
||||||
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
if (immersiveMode) {
|
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||||
WindowInsetsControllerCompat(
|
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||||
window, window.decorView
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
).hide(WindowInsetsCompat.Type.statusBars())
|
|
||||||
}
|
|
||||||
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
}
|
|
||||||
val typedValue = TypedValue()
|
|
||||||
val theme = requireContext().theme
|
|
||||||
theme.resolveAttribute(
|
|
||||||
com.google.android.material.R.attr.colorSurface,
|
|
||||||
typedValue,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
window.navigationBarColor = typedValue.data
|
|
||||||
}
|
}
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
val theme = requireContext().theme
|
||||||
|
theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSurface,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
window.navigationBarColor = typedValue.data
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun show(manager: FragmentManager, tag: String?) {
|
override fun show(manager: FragmentManager, tag: String?) {
|
||||||
@@ -361,35 +202,21 @@ fun isOnline(context: Context): Boolean {
|
|||||||
val connectivityManager =
|
val connectivityManager =
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
return tryWith {
|
return tryWith {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
return@tryWith if (cap != null) {
|
||||||
return@tryWith if (cap != null) {
|
when {
|
||||||
when {
|
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
||||||
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
||||||
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
||||||
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
||||||
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
cap.hasTransport(TRANSPORT_USB) ||
|
||||||
cap.hasTransport(TRANSPORT_USB) ||
|
cap.hasTransport(TRANSPORT_VPN) ||
|
||||||
cap.hasTransport(TRANSPORT_VPN) ||
|
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
} else false
|
} else false
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
return@tryWith connectivityManager.activeNetworkInfo?.run {
|
|
||||||
type == ConnectivityManager.TYPE_BLUETOOTH ||
|
|
||||||
type == ConnectivityManager.TYPE_ETHERNET ||
|
|
||||||
type == ConnectivityManager.TYPE_MOBILE ||
|
|
||||||
type == ConnectivityManager.TYPE_MOBILE_DUN ||
|
|
||||||
type == ConnectivityManager.TYPE_MOBILE_HIPRI ||
|
|
||||||
type == ConnectivityManager.TYPE_WIFI ||
|
|
||||||
type == ConnectivityManager.TYPE_WIMAX ||
|
|
||||||
type == ConnectivityManager.TYPE_VPN
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +248,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)
|
||||||
) { _, which ->
|
) { dialog, which ->
|
||||||
if (which == DialogInterface.BUTTON_NEUTRAL) {
|
if (which == DialogInterface.BUTTON_NEUTRAL) {
|
||||||
date = FuzzyDate()
|
date = FuzzyDate()
|
||||||
}
|
}
|
||||||
@@ -451,11 +278,12 @@ class InputFilterMinMax(
|
|||||||
val input = (dest.toString() + source.toString()).toDouble()
|
val input = (dest.toString() + source.toString()).toDouble()
|
||||||
if (isInRange(min, max, input)) return null
|
if (isInRange(min, max, input)) return null
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (nfe: NumberFormatException) {
|
||||||
Logger.log(nfe)
|
logger(nfe.stackTraceToString())
|
||||||
}
|
}
|
||||||
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]
|
||||||
|
|
||||||
@@ -468,7 +296,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)) {
|
||||||
@@ -621,17 +449,11 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
|
||||||
if (file?.url?.isNotEmpty() == true) {
|
if (file?.url?.isNotEmpty() == true) {
|
||||||
tryWith {
|
tryWith {
|
||||||
if (file.url.startsWith("content://")) {
|
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||||
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
|
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||||
.override(size).into(this)
|
.into(this)
|
||||||
} else {
|
|
||||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
|
||||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
|
||||||
.into(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,41 +579,9 @@ fun View.circularReveal(ex: Int, ey: Int, subX: Boolean, time: Long) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openLinkInBrowser(link: String?) {
|
fun openLinkInBrowser(link: String?) {
|
||||||
link?.let {
|
tryWith {
|
||||||
try {
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
||||||
val emptyBrowserIntent = Intent(Intent.ACTION_VIEW).apply {
|
currContext()?.startActivity(intent)
|
||||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
|
||||||
data = Uri.fromParts("http", "", null)
|
|
||||||
}
|
|
||||||
val sendIntent = Intent().apply {
|
|
||||||
action = Intent.ACTION_VIEW
|
|
||||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
|
||||||
data = Uri.parse(link)
|
|
||||||
selector = emptyBrowserIntent
|
|
||||||
}
|
|
||||||
currContext()!!.startActivity(sendIntent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
snackString("No browser found")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,6 +676,26 @@ fun savePrefs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
||||||
|
val permissions = arrayOf(
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
|
|
||||||
|
val requiredPermissions = permissions.filter {
|
||||||
|
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
return if (requiredPermissions.isNotEmpty()) {
|
||||||
|
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
|
||||||
|
|
||||||
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||||
|
|
||||||
val contentUri = FileProvider.getUriForFile(
|
val contentUri = FileProvider.getUriForFile(
|
||||||
@@ -918,7 +728,7 @@ fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
|||||||
|
|
||||||
private fun scanFile(path: String, context: Context) {
|
private fun scanFile(path: String, context: Context) {
|
||||||
MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ ->
|
MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ ->
|
||||||
Logger.log("Finished scanning $p")
|
logger("Finished scanning $p")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -950,15 +760,12 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
|||||||
val clipboard = getSystemService(activity, ClipboardManager::class.java)
|
val clipboard = getSystemService(activity, ClipboardManager::class.java)
|
||||||
val clip = ClipData.newPlainText("label", string)
|
val clip = ClipData.newPlainText("label", string)
|
||||||
clipboard?.setPrimaryClip(clip)
|
clipboard?.setPrimaryClip(clip)
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
if (toast) snackString(activity.getString(R.string.copied_text, string))
|
||||||
if (toast) snackString(activity.getString(R.string.copied_text, string))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
fun countDown(media: Media, view: ViewGroup) {
|
fun countDown(media: Media, view: ViewGroup) {
|
||||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
|
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
|
||||||
&& (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 =
|
||||||
@@ -990,50 +797,6 @@ 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) {
|
||||||
@@ -1103,13 +866,9 @@ 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(string)
|
||||||
MainScope().launch {
|
MainScope().launch {
|
||||||
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
|
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
@@ -1117,20 +876,16 @@ fun toast(string: String?) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toast(res: Int) {
|
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
|
||||||
toast(getAppString(res))
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
(activity ?: currActivity())?.apply {
|
(activity ?: currActivity())?.apply {
|
||||||
val snackBar = Snackbar.make(
|
|
||||||
window.decorView.findViewById(android.R.id.content),
|
|
||||||
s,
|
|
||||||
Snackbar.LENGTH_SHORT
|
|
||||||
)
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
|
val snackBar = Snackbar.make(
|
||||||
|
window.decorView.findViewById(android.R.id.content),
|
||||||
|
s,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
)
|
||||||
snackBar.view.apply {
|
snackBar.view.apply {
|
||||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||||
@@ -1150,19 +905,13 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
|
|||||||
}
|
}
|
||||||
snackBar.show()
|
snackBar.show()
|
||||||
}
|
}
|
||||||
return snackBar
|
|
||||||
}
|
}
|
||||||
Logger.log(s)
|
logger(s)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log(e)
|
logger(e.stackTraceToString())
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
}
|
}
|
||||||
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>) :
|
||||||
@@ -1288,7 +1037,7 @@ fun incognitoNotification(context: Context) {
|
|||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||||
if (incognito) {
|
if (incognito) {
|
||||||
val intent = Intent(context, IncognitoNotificationClickReceiver::class.java)
|
val intent = Intent(context, NotificationClickReceiver::class.java)
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
context, 0, intent,
|
context, 0, intent,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
@@ -1306,28 +1055,6 @@ fun incognitoNotification(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasNotificationPermission(context: Context): Boolean {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
|
||||||
} else {
|
|
||||||
NotificationManagerCompat.from(context).areNotificationsEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openSettings(context: Context, channelId: String?): Boolean {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val intent = Intent(
|
|
||||||
if (channelId != null) Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
|
|
||||||
else Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
|
||||||
).apply {
|
|
||||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
|
||||||
putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun View.pop() {
|
suspend fun View.pop() {
|
||||||
currActivity()?.runOnUiThread {
|
currActivity()?.runOnUiThread {
|
||||||
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
|
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
|
||||||
@@ -1340,104 +1067,3 @@ suspend fun View.pop() {
|
|||||||
}
|
}
|
||||||
delay(100)
|
delay(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun blurImage(imageView: ImageView, banner: String?) {
|
|
||||||
if (banner != null) {
|
|
||||||
val radius = PrefManager.getVal<Float>(PrefName.BlurRadius).toInt()
|
|
||||||
val sampling = PrefManager.getVal<Float>(PrefName.BlurSampling).toInt()
|
|
||||||
if (PrefManager.getVal(PrefName.BlurBanners)) {
|
|
||||||
val context = imageView.context
|
|
||||||
if (!(context as Activity).isDestroyed) {
|
|
||||||
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
|
|
||||||
Glide.with(context as Context)
|
|
||||||
.load(
|
|
||||||
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)))
|
|
||||||
.into(imageView)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
imageView.loadImage(banner)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
imageView.setImageResource(R.drawable.linear_gradient_bg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the markwon instance with all the plugins
|
|
||||||
* @return the markwon instance
|
|
||||||
*/
|
|
||||||
fun buildMarkwon(
|
|
||||||
activity: Context,
|
|
||||||
userInputContent: Boolean = true,
|
|
||||||
fragment: Fragment? = null
|
|
||||||
): Markwon {
|
|
||||||
val glideContext = fragment?.let { Glide.with(it) } ?: Glide.with(activity)
|
|
||||||
val markwon = Markwon.builder(activity)
|
|
||||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
|
||||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
|
||||||
builder.linkResolver { _, link ->
|
|
||||||
copyToClipboard(link, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
|
||||||
.usePlugin(StrikethroughPlugin.create())
|
|
||||||
.usePlugin(TablePlugin.create(activity))
|
|
||||||
.usePlugin(TaskListPlugin.create(activity))
|
|
||||||
.usePlugin(SpoilerPlugin())
|
|
||||||
.usePlugin(HtmlPlugin.create { plugin ->
|
|
||||||
if (userInputContent) {
|
|
||||||
plugin.addHandler(
|
|
||||||
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
|
|
||||||
|
|
||||||
private val requestManager: RequestManager = glideContext.apply {
|
|
||||||
addDefaultRequestListener(object : RequestListener<Any> {
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: Any,
|
|
||||||
model: Any,
|
|
||||||
target: Target<Any>,
|
|
||||||
dataSource: DataSource,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
if (resource is GifDrawable) {
|
|
||||||
resource.start()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<Any>,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
Logger.log("Image failed to load: $model")
|
|
||||||
Logger.log(e as Exception)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
|
|
||||||
Logger.log("Loading image: ${drawable.destination}")
|
|
||||||
return requestManager.load(drawable.destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel(target: Target<*>) {
|
|
||||||
Logger.log("Cancelling image load")
|
|
||||||
requestManager.clear(target)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.build()
|
|
||||||
return markwon
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package ani.dantotsu
|
|||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
|
||||||
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
|
||||||
@@ -13,11 +11,12 @@ 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.view.LayoutInflater
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnticipateInterpolator
|
import android.view.animation.AnticipateInterpolator
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
@@ -26,55 +25,40 @@ import androidx.core.animation.doOnEnd
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.doOnAttach
|
import androidx.core.view.doOnAttach
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updateMargins
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
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
|
||||||
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 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.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
|
||||||
import ani.dantotsu.home.MangaFragment
|
import ani.dantotsu.home.MangaFragment
|
||||||
import ani.dantotsu.home.NoInternet
|
import ani.dantotsu.home.NoInternet
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
|
||||||
import ani.dantotsu.notifications.comment.CommentNotificationWorker
|
|
||||||
import ani.dantotsu.others.CustomBottomDialog
|
import ani.dantotsu.others.CustomBottomDialog
|
||||||
import ani.dantotsu.profile.ProfileActivity
|
|
||||||
import ani.dantotsu.profile.activity.FeedActivity
|
|
||||||
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
|
||||||
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
|
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
|
||||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
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.GlobalScope
|
||||||
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,7 +71,6 @@ 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?) {
|
||||||
@@ -101,77 +84,17 @@ class MainActivity : AppCompatActivity() {
|
|||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||||
androidx.work.WorkManager.getInstance(this)
|
|
||||||
.enqueue(OneTimeWorkRequest.Companion.from(CommentNotificationWorker::class.java))
|
|
||||||
|
|
||||||
androidx.work.WorkManager.getInstance(this)
|
|
||||||
.enqueue(OneTimeWorkRequest.Companion.from(AnilistNotificationWorker::class.java))
|
|
||||||
|
|
||||||
val action = intent.action
|
|
||||||
val type = intent.type
|
|
||||||
if (Intent.ACTION_VIEW == action && type != null) {
|
|
||||||
val uri: Uri? = intent.data
|
|
||||||
try {
|
|
||||||
if (uri == null) {
|
|
||||||
throw Exception("Uri is null")
|
|
||||||
}
|
|
||||||
val jsonString =
|
|
||||||
contentResolver.openInputStream(uri)?.readBytes()
|
|
||||||
?: throw Exception("Error reading file")
|
|
||||||
val name =
|
|
||||||
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
|
|
||||||
//.sani is encrypted, .ani is not
|
|
||||||
if (name.endsWith(".sani")) {
|
|
||||||
passwordAlertDialog { password ->
|
|
||||||
if (password != null) {
|
|
||||||
val salt = jsonString.copyOfRange(0, 16)
|
|
||||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
|
||||||
val decryptedJson = try {
|
|
||||||
PreferenceKeystore.decryptWithPassword(
|
|
||||||
password,
|
|
||||||
encrypted,
|
|
||||||
salt
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
toast("Incorrect password")
|
|
||||||
return@passwordAlertDialog
|
|
||||||
}
|
|
||||||
if (PreferencePackager.unpack(decryptedJson)) {
|
|
||||||
val intent = Intent(this, this.javaClass)
|
|
||||||
this.finish()
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast("Password cannot be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (name.endsWith(".ani")) {
|
|
||||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
|
||||||
if (PreferencePackager.unpack(decryptedJson)) {
|
|
||||||
val intent = Intent(this, this.javaClass)
|
|
||||||
this.finish()
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast("Invalid file type")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
toast("Error importing settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = bottomNavBar.background as GradientDrawable
|
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||||
backgroundDrawable.setColor(semiTransparentColor)
|
backgroundDrawable.setColor(semiTransparentColor)
|
||||||
bottomNavBar.background = backgroundDrawable
|
_bottomBar.background = backgroundDrawable
|
||||||
}
|
}
|
||||||
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
_bottomBar.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")
|
||||||
@@ -221,14 +144,22 @@ class MainActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
doubleBackToExitPressedOnce = true
|
doubleBackToExitPressedOnce = true
|
||||||
snackString(this@MainActivity.getString(R.string.back_to_exit)).apply {
|
snackString(this@MainActivity.getString(R.string.back_to_exit))
|
||||||
this?.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
Handler(Looper.getMainLooper()).postDelayed(
|
||||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
{ doubleBackToExitPressedOnce = false },
|
||||||
super.onDismissed(transientBottomBar, event)
|
2000
|
||||||
doubleBackToExitPressedOnce = false
|
)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
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
|
||||||
@@ -274,17 +205,6 @@ 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)
|
|
||||||
selectedOption = if (fragment != null) {
|
selectedOption = if (fragment != null) {
|
||||||
when (fragment) {
|
when (fragment) {
|
||||||
AnimeFragment::class.java.name -> 0
|
AnimeFragment::class.java.name -> 0
|
||||||
@@ -297,40 +217,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = navBarHeight
|
bottomMargin = navBarHeight
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var launched = false
|
|
||||||
intent.extras?.let { extras ->
|
|
||||||
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
|
|
||||||
val mediaId = extras.getInt("mediaId", -1)
|
|
||||||
val commentId = extras.getInt("commentId", -1)
|
|
||||||
val activityId = extras.getInt("activityId", -1)
|
|
||||||
|
|
||||||
if (fragmentToLoad != null && mediaId != -1 && commentId != -1) {
|
|
||||||
val detailIntent = Intent(this, MediaDetailsActivity::class.java).apply {
|
|
||||||
putExtra("FRAGMENT_TO_LOAD", fragmentToLoad)
|
|
||||||
putExtra("mediaId", mediaId)
|
|
||||||
putExtra("commentId", commentId)
|
|
||||||
}
|
|
||||||
launched = true
|
|
||||||
startActivity(detailIntent)
|
|
||||||
} else if (fragmentToLoad == "FEED" && activityId != -1) {
|
|
||||||
val feedIntent = Intent(this, FeedActivity::class.java).apply {
|
|
||||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
|
||||||
putExtra("activityId", activityId)
|
|
||||||
|
|
||||||
}
|
|
||||||
launched = true
|
|
||||||
startActivity(feedIntent)
|
|
||||||
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
|
||||||
Logger.log("MainActivity, onCreate: $activityId")
|
|
||||||
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
|
|
||||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
|
||||||
putExtra("activityId", activityId)
|
|
||||||
}
|
|
||||||
launched = true
|
|
||||||
startActivity(notificationIntent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||||
@@ -343,7 +230,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) {
|
model.genres.observe(this) { it ->
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
if (it) {
|
if (it) {
|
||||||
val navbar = binding.includedNavbar.navbar
|
val navbar = binding.includedNavbar.navbar
|
||||||
@@ -368,14 +255,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
mainViewPager.setCurrentItem(newIndex, false)
|
mainViewPager.setCurrentItem(newIndex, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (mainViewPager.currentItem != selectedOption) {
|
navbar.selectTabAt(selectedOption)
|
||||||
navbar.selectTabAt(selectedOption)
|
mainViewPager.post {
|
||||||
mainViewPager.post {
|
mainViewPager.setCurrentItem(
|
||||||
mainViewPager.setCurrentItem(
|
selectedOption,
|
||||||
selectedOption,
|
false
|
||||||
false
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.mainProgressBar.visibility = View.GONE
|
binding.mainProgressBar.visibility = View.GONE
|
||||||
@@ -383,7 +268,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Load Data
|
//Load Data
|
||||||
if (!load && !launched) {
|
if (!load) {
|
||||||
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)
|
||||||
@@ -403,21 +288,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val username = intent.extras?.getString("username")
|
delay(500)
|
||||||
if (username != null) {
|
startSubscription()
|
||||||
val nameInt = username.toIntOrNull()
|
|
||||||
if (nameInt != null) {
|
|
||||||
startActivity(
|
|
||||||
Intent(this@MainActivity, ProfileActivity::class.java)
|
|
||||||
.putExtra("userId", nameInt)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
startActivity(
|
|
||||||
Intent(this@MainActivity, ProfileActivity::class.java)
|
|
||||||
.putExtra("username", username)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
load = true
|
load = true
|
||||||
}
|
}
|
||||||
@@ -454,78 +326,27 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//TODO: Remove this
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||||
|
val downloadCursor = index.getDownloads()
|
||||||
|
while (downloadCursor.moveToNext()) {
|
||||||
|
val download = downloadCursor.download
|
||||||
|
Log.e("Downloader", download.request.uri.toString())
|
||||||
|
Log.e("Downloader", download.request.id)
|
||||||
|
Log.e("Downloader", download.request.mimeType.toString())
|
||||||
|
Log.e("Downloader", download.request.data.size.toString())
|
||||||
|
Log.e("Downloader", download.bytesDownloaded.toString())
|
||||||
|
Log.e("Downloader", download.state.toString())
|
||||||
|
Log.e("Downloader", download.failureReason.toString())
|
||||||
|
|
||||||
val torrentManager = Injekt.get<TorrentAddonManager>()
|
if (download.state == Download.STATE_FAILED) { //simple cleanup
|
||||||
fun startTorrent() {
|
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||||
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
|
|
||||||
launchIO {
|
|
||||||
if (!ServerService.isRunning()) {
|
|
||||||
ServerService.start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (torrentManager.isInitialized.value == false) {
|
|
||||||
torrentManager.isInitialized.observe(this) {
|
|
||||||
if (it) {
|
|
||||||
startTorrent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startTorrent()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestart() {
|
|
||||||
super.onRestart()
|
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
|
|
||||||
val params: ViewGroup.MarginLayoutParams =
|
|
||||||
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
params.updateMargins(bottom = margin.toPx)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
|
||||||
val password = CharArray(16).apply { fill('0') }
|
|
||||||
|
|
||||||
// Inflate the dialog layout
|
|
||||||
val dialogView =
|
|
||||||
LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null)
|
|
||||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
|
||||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
|
||||||
subtitleTextView?.visibility = View.VISIBLE
|
|
||||||
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
|
||||||
.setTitle("Enter Password")
|
|
||||||
.setView(dialogView)
|
|
||||||
.setPositiveButton("OK", null)
|
|
||||||
.setNegativeButton("Cancel") { dialog, _ ->
|
|
||||||
password.fill('0')
|
|
||||||
dialog.dismiss()
|
|
||||||
callback(null)
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
|
|
||||||
dialog.window?.setDimAmount(0.8f)
|
|
||||||
dialog.show()
|
|
||||||
|
|
||||||
// Override the positive button here
|
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
|
||||||
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
|
|
||||||
if (editText?.text?.isNotBlank() == true) {
|
|
||||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
|
||||||
dialog.dismiss()
|
|
||||||
callback(password)
|
|
||||||
} else {
|
|
||||||
toast("Password cannot be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//ViewPager
|
//ViewPager
|
||||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
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
|
||||||
import ani.dantotsu.others.webview.WebViewBottomDialog
|
import ani.dantotsu.others.webview.WebViewBottomDialog
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import com.lagradost.nicehttp.Requests
|
import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.ResponseParser
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
import com.lagradost.nicehttp.addGenericDns
|
import com.lagradost.nicehttp.addGenericDns
|
||||||
import 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 +34,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() {
|
fun initializeNetwork(context: Context) {
|
||||||
|
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
|
||||||
defaultHeaders = mapOf(
|
defaultHeaders = mapOf(
|
||||||
"User-Agent" to
|
"User-Agent" to
|
||||||
defaultUserAgentProvider()
|
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
||||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,7 +104,6 @@ fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
|
|||||||
toast(e.localizedMessage)
|
toast(e.localizedMessage)
|
||||||
}
|
}
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
Logger.log(e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? {
|
fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? {
|
||||||
@@ -136,7 +134,7 @@ suspend fun <T> tryWithSuspend(
|
|||||||
* A url, which can also have headers
|
* A url, which can also have headers
|
||||||
* **/
|
* **/
|
||||||
data class FileUrl(
|
data class FileUrl(
|
||||||
var url: String,
|
val url: String,
|
||||||
val headers: Map<String, String> = mapOf()
|
val headers: Map<String, String> = mapOf()
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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())
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package ani.dantotsu.addons
|
|
||||||
|
|
||||||
interface AddonListener {
|
|
||||||
fun onAddonInstalled(result: LoadResult?)
|
|
||||||
fun onAddonUpdated(result: LoadResult?)
|
|
||||||
fun onAddonUninstalled(pkgName: String)
|
|
||||||
|
|
||||||
enum class ListenerAction {
|
|
||||||
INSTALL, UPDATE, UNINSTALL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package ani.dantotsu.addons
|
|
||||||
|
|
||||||
abstract class LoadResult {
|
|
||||||
|
|
||||||
abstract class Success : LoadResult()
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package ani.dantotsu.addons.download
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import ani.dantotsu.addons.AddonListener
|
|
||||||
import ani.dantotsu.addons.AddonLoader
|
|
||||||
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 -> {
|
|
||||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
|
||||||
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 -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
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.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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package ani.dantotsu.addons.download
|
|
||||||
|
|
||||||
import ani.dantotsu.addons.LoadResult
|
|
||||||
|
|
||||||
open class DownloadLoadResult : LoadResult() {
|
|
||||||
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
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.download.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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package ani.dantotsu.addons.torrent
|
|
||||||
|
|
||||||
import ani.dantotsu.addons.LoadResult
|
|
||||||
|
|
||||||
open class TorrentLoadResult : LoadResult() {
|
|
||||||
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
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,8 +6,6 @@ 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
|
||||||
@@ -20,7 +18,6 @@ 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
|
||||||
@@ -32,7 +29,6 @@ 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)
|
||||||
@@ -40,13 +36,10 @@ 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()) }
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ fun updateProgress(media: Media, number: String) {
|
|||||||
if (Anilist.userid != null) {
|
if (Anilist.userid != null) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val a = number.toFloatOrNull()?.toInt()
|
val a = number.toFloatOrNull()?.toInt()
|
||||||
if ((a ?: 0) > (media.userProgress ?: -1)) {
|
if ((a ?: 0) > (media.userProgress ?: 0)) {
|
||||||
Anilist.mutation.editList(
|
Anilist.mutation.editList(
|
||||||
media.id,
|
media.id,
|
||||||
a,
|
a,
|
||||||
|
|||||||
@@ -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.toast
|
import ani.dantotsu.toast
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.tryWithSuspend
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
object Anilist {
|
object Anilist {
|
||||||
@@ -28,7 +27,6 @@ object Anilist {
|
|||||||
var bg: String? = null
|
var bg: String? = null
|
||||||
var episodesWatched: Int? = null
|
var episodesWatched: Int? = null
|
||||||
var chapterRead: Int? = null
|
var chapterRead: Int? = null
|
||||||
var unreadNotificationCount: Int = 0
|
|
||||||
|
|
||||||
var genres: ArrayList<String>? = null
|
var genres: ArrayList<String>? = null
|
||||||
var tags: Map<Boolean, List<String>>? = null
|
var tags: Map<Boolean, List<String>>? = null
|
||||||
@@ -39,54 +37,20 @@ 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 animeFormats = listOf(
|
val anime_formats = listOf(
|
||||||
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
||||||
)
|
)
|
||||||
|
|
||||||
val mangaFormats = listOf(
|
val manga_formats = listOf(
|
||||||
"MANGA", "NOVEL", "ONE SHOT"
|
"MANGA", "NOVEL", "ONE SHOT"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -150,9 +114,6 @@ 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,8 +124,7 @@ object Anilist {
|
|||||||
show: Boolean = false,
|
show: Boolean = false,
|
||||||
cache: Int? = null
|
cache: Int? = null
|
||||||
): T? {
|
): T? {
|
||||||
return try {
|
return tryWithSuspend {
|
||||||
if (show) Logger.log("Anilist Query: $query")
|
|
||||||
if (rateLimitReset > System.currentTimeMillis() / 1000) {
|
if (rateLimitReset > System.currentTimeMillis() / 1000) {
|
||||||
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||||
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||||
@@ -188,7 +148,7 @@ object Anilist {
|
|||||||
cacheTime = cache ?: 10
|
cacheTime = cache ?: 10
|
||||||
)
|
)
|
||||||
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
|
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
|
||||||
Logger.log("Remaining requests: $remaining")
|
Log.d("AnilistQuery", "Remaining requests: $remaining")
|
||||||
if (json.code == 429) {
|
if (json.code == 429) {
|
||||||
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
|
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
|
||||||
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
|
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
|
||||||
@@ -199,16 +159,10 @@ 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("{")) {
|
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||||
throw Exception(currContext()?.getString(R.string.anilist_down))
|
if (show) println("Response : ${json.text}")
|
||||||
}
|
|
||||||
if (show) Logger.log("Anilist Response: ${json.text}")
|
|
||||||
json.parsed()
|
json.parsed()
|
||||||
} else null
|
} else null
|
||||||
} catch (e: Exception) {
|
|
||||||
if (show) snackString("Error fetching Anilist data: ${e.message}")
|
|
||||||
Logger.log("Anilist Query Error: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,23 +13,6 @@ class AnilistMutations {
|
|||||||
executeQuery<JsonObject>(query, variables)
|
executeQuery<JsonObject>(query, variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toggleFav(type: FavType, id: Int): Boolean {
|
|
||||||
val filter = when (type) {
|
|
||||||
FavType.ANIME -> "animeId"
|
|
||||||
FavType.MANGA -> "mangaId"
|
|
||||||
FavType.CHARACTER -> "characterId"
|
|
||||||
FavType.STAFF -> "staffId"
|
|
||||||
FavType.STUDIO -> "studioId"
|
|
||||||
}
|
|
||||||
val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}"""
|
|
||||||
val result = executeQuery<JsonObject>(query)
|
|
||||||
return result?.get("errors") == null && result != null
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class FavType {
|
|
||||||
ANIME, MANGA, CHARACTER, STAFF, STUDIO
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun editList(
|
suspend fun editList(
|
||||||
mediaID: Int,
|
mediaID: Int,
|
||||||
progress: Int? = null,
|
progress: Int? = null,
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ import ani.dantotsu.checkGenreTime
|
|||||||
import ani.dantotsu.checkId
|
import ani.dantotsu.checkId
|
||||||
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
||||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||||
import ani.dantotsu.connections.anilist.api.FeedResponse
|
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
import ani.dantotsu.connections.anilist.api.NotificationResponse
|
|
||||||
import ani.dantotsu.connections.anilist.api.Page
|
import ani.dantotsu.connections.anilist.api.Page
|
||||||
import ani.dantotsu.connections.anilist.api.Query
|
import ani.dantotsu.connections.anilist.api.Query
|
||||||
import ani.dantotsu.connections.anilist.api.ToggleLike
|
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.isOnline
|
import ani.dantotsu.isOnline
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
@@ -20,11 +17,9 @@ 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
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -36,27 +31,23 @@ import java.io.Serializable
|
|||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class AnilistQueries {
|
class AnilistQueries {
|
||||||
|
|
||||||
suspend fun getUserData(): Boolean {
|
suspend fun getUserData(): Boolean {
|
||||||
val response: Query.Viewer?
|
val response: Query.Viewer?
|
||||||
measureTimeMillis {
|
measureTimeMillis {
|
||||||
response =
|
response =
|
||||||
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""")
|
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}}}""")
|
||||||
}.also { println("time : $it") }
|
}.also { println("time : $it") }
|
||||||
val user = response?.data?.user ?: return false
|
val user = response?.data?.user ?: return false
|
||||||
|
|
||||||
PrefManager.setVal(PrefName.AnilistUserName, user.name)
|
PrefManager.setVal(PrefName.AnilistUserName, user.name)
|
||||||
|
|
||||||
Anilist.userid = user.id
|
Anilist.userid = user.id
|
||||||
PrefManager.setVal(PrefName.AnilistUserId, user.id.toString())
|
|
||||||
Anilist.username = user.name
|
Anilist.username = user.name
|
||||||
Anilist.bg = user.bannerImage
|
Anilist.bg = user.bannerImage
|
||||||
Anilist.avatar = user.avatar?.medium
|
Anilist.avatar = user.avatar?.medium
|
||||||
Anilist.episodesWatched = user.statistics?.anime?.episodesWatched
|
Anilist.episodesWatched = user.statistics?.anime?.episodesWatched
|
||||||
Anilist.chapterRead = user.statistics?.manga?.chaptersRead
|
Anilist.chapterRead = user.statistics?.manga?.chaptersRead
|
||||||
Anilist.adult = user.options?.displayAdultContent ?: false
|
Anilist.adult = user.options?.displayAdultContent ?: false
|
||||||
Anilist.unreadNotificationCount = user.unreadNotificationCount ?: 0
|
|
||||||
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
|
|
||||||
Anilist.unreadNotificationCount += unread
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,19 +64,18 @@ class AnilistQueries {
|
|||||||
media.cameFromContinue = false
|
media.cameFromContinue = false
|
||||||
|
|
||||||
val query =
|
val query =
|
||||||
"""{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}}}}}"""
|
"""{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}}}}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 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}}}"""
|
||||||
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
|
||||||
|
|
||||||
@@ -131,38 +121,6 @@ class AnilistQueries {
|
|||||||
name = i.node?.name?.userPreferred,
|
name = i.node?.name?.userPreferred,
|
||||||
image = i.node?.image?.medium,
|
image = i.node?.image?.medium,
|
||||||
banner = media.banner ?: media.cover,
|
banner = media.banner ?: media.cover,
|
||||||
isFav = i.node?.isFavourite ?: false,
|
|
||||||
role = when (i.role.toString()) {
|
|
||||||
"MAIN" -> currContext()?.getString(R.string.main_role)
|
|
||||||
?: "MAIN"
|
|
||||||
|
|
||||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
|
|
||||||
?: "SUPPORTING"
|
|
||||||
|
|
||||||
else -> i.role.toString()
|
|
||||||
},
|
|
||||||
voiceActor = i.voiceActors?.map {
|
|
||||||
Author(
|
|
||||||
id = it.id,
|
|
||||||
name = it.name?.userPreferred,
|
|
||||||
image = it.image?.large,
|
|
||||||
role = it.languageV2
|
|
||||||
)
|
|
||||||
} as ArrayList<Author>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fetchedMedia.staff != null) {
|
|
||||||
media.staff = arrayListOf()
|
|
||||||
fetchedMedia.staff?.edges?.forEach { i ->
|
|
||||||
i.node?.apply {
|
|
||||||
media.staff?.add(
|
|
||||||
Author(
|
|
||||||
id = id,
|
|
||||||
name = i.node?.name?.userPreferred,
|
|
||||||
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"
|
||||||
@@ -209,24 +167,7 @@ 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
|
||||||
@@ -270,10 +211,8 @@ class AnilistQueries {
|
|||||||
|
|
||||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||||
media.anime.author = Author(
|
media.anime.author = Author(
|
||||||
it.id,
|
it.id.toString(),
|
||||||
it.name?.userPreferred ?: "N/A",
|
it.name?.userPreferred ?: "N/A"
|
||||||
it.image?.medium,
|
|
||||||
"AUTHOR"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,10 +231,8 @@ class AnilistQueries {
|
|||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||||
media.manga.author = Author(
|
media.manga.author = Author(
|
||||||
it.id,
|
it.id.toString(),
|
||||||
it.name?.userPreferred ?: "N/A",
|
it.name?.userPreferred ?: "N/A"
|
||||||
it.image?.medium,
|
|
||||||
"AUTHOR"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,7 +249,6 @@ class AnilistQueries {
|
|||||||
} else {
|
} else {
|
||||||
if (currContext()?.let { isOnline(it) } == true) {
|
if (currContext()?.let { isOnline(it) } == true) {
|
||||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,52 +262,6 @@ class AnilistQueries {
|
|||||||
return media
|
return media
|
||||||
}
|
}
|
||||||
|
|
||||||
fun userMediaDetails(media: Media): Media {
|
|
||||||
val query =
|
|
||||||
"""{Media(id:${media.id}){id mediaListEntry{id status progress private repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite idMal}}"""
|
|
||||||
runBlocking {
|
|
||||||
val anilist = async {
|
|
||||||
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
|
||||||
if (response != null) {
|
|
||||||
fun parse() {
|
|
||||||
val fetchedMedia = response?.data?.media ?: return
|
|
||||||
|
|
||||||
if (fetchedMedia.mediaListEntry != null) {
|
|
||||||
fetchedMedia.mediaListEntry?.apply {
|
|
||||||
media.userProgress = progress
|
|
||||||
media.isListPrivate = private ?: false
|
|
||||||
media.userListId = id
|
|
||||||
media.userStatus = status?.toString()
|
|
||||||
media.inCustomListsOf = customLists?.toMutableMap()
|
|
||||||
media.userRepeat = repeat ?: 0
|
|
||||||
media.userUpdatedAt = updatedAt?.toString()?.toLong()?.times(1000)
|
|
||||||
media.userCompletedAt = completedAt ?: FuzzyDate()
|
|
||||||
media.userStartedAt = startedAt ?: FuzzyDate()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
media.isListPrivate = false
|
|
||||||
media.userStatus = null
|
|
||||||
media.userListId = null
|
|
||||||
media.userProgress = null
|
|
||||||
media.userRepeat = 0
|
|
||||||
media.userUpdatedAt = null
|
|
||||||
media.userCompletedAt = FuzzyDate()
|
|
||||||
media.userStartedAt = FuzzyDate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data?.media != null) parse()
|
|
||||||
else {
|
|
||||||
response = executeQuery(query, force = true, useToken = false)
|
|
||||||
if (response?.data?.media != null) parse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
awaitAll(anilist)
|
|
||||||
}
|
|
||||||
return media
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
|
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
|
||||||
val returnArray = arrayListOf<Media>()
|
val returnArray = arrayListOf<Media>()
|
||||||
val map = mutableMapOf<Int, Media>()
|
val map = mutableMapOf<Int, Media>()
|
||||||
@@ -409,18 +299,9 @@ class AnilistQueries {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type != "ANIME") {
|
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||||
returnArray.addAll(map.values)
|
if (set.isNotEmpty()) {
|
||||||
return returnArray
|
set.reversed().forEach {
|
||||||
}
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val list = PrefManager.getNullableCustomVal(
|
|
||||||
"continueAnimeList",
|
|
||||||
listOf<Int>(),
|
|
||||||
List::class.java
|
|
||||||
) as List<Int>
|
|
||||||
if (list.isNotEmpty()) {
|
|
||||||
list.reversed().forEach {
|
|
||||||
if (map.containsKey(it)) returnArray.add(map[it]!!)
|
if (map.containsKey(it)) returnArray.add(map[it]!!)
|
||||||
}
|
}
|
||||||
for (i in map) {
|
for (i in map) {
|
||||||
@@ -434,12 +315,12 @@ class AnilistQueries {
|
|||||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
|
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
|
suspend fun favMedia(anime: Boolean): ArrayList<Media> {
|
||||||
var hasNextPage = true
|
var hasNextPage = true
|
||||||
var page = 0
|
var page = 0
|
||||||
|
|
||||||
suspend fun getNextPage(page: Int): List<Media> {
|
suspend fun getNextPage(page: Int): List<Media> {
|
||||||
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page, id)}}""")
|
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page)}}""")
|
||||||
val favourites = response?.data?.user?.favourites
|
val favourites = response?.data?.user?.favourites
|
||||||
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
||||||
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
||||||
@@ -458,8 +339,8 @@ class AnilistQueries {
|
|||||||
return responseArray
|
return responseArray
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String {
|
private fun favMediaQuery(anime: Boolean, page: Int): String {
|
||||||
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
return """User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun recommendations(): ArrayList<Media> {
|
suspend fun recommendations(): ArrayList<Media> {
|
||||||
@@ -502,7 +383,7 @@ class AnilistQueries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun recommendationPlannedQuery(type: String): String {
|
private fun recommendationPlannedQuery(type: String): String {
|
||||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
|
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
|
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
|
||||||
@@ -542,8 +423,7 @@ class AnilistQueries {
|
|||||||
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
|
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
|
||||||
query += """}""".trimEnd(',')
|
query += """}""".trimEnd(',')
|
||||||
|
|
||||||
val response = executeQuery<Query.HomePageMedia>(query, show = true)
|
val response = executeQuery<Query.HomePageMedia>(query)
|
||||||
Logger.log(response.toString())
|
|
||||||
val returnMap = mutableMapOf<String, ArrayList<Media>>()
|
val returnMap = mutableMapOf<String, ArrayList<Media>>()
|
||||||
fun current(type: String) {
|
fun current(type: String) {
|
||||||
val subMap = mutableMapOf<Int, Media>()
|
val subMap = mutableMapOf<Int, Media>()
|
||||||
@@ -566,19 +446,10 @@ class AnilistQueries {
|
|||||||
subMap[m.id] = m
|
subMap[m.id] = m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type != "Anime") {
|
val set = PrefManager.getCustomVal<Set<Int>>("continue_${type.uppercase()}", setOf())
|
||||||
returnArray.addAll(subMap.values)
|
.toMutableSet()
|
||||||
returnMap["current$type"] = returnArray
|
if (set.isNotEmpty()) {
|
||||||
return
|
set.reversed().forEach {
|
||||||
}
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val list = PrefManager.getNullableCustomVal(
|
|
||||||
"continueAnimeList",
|
|
||||||
listOf<Int>(),
|
|
||||||
List::class.java
|
|
||||||
) as List<Int>
|
|
||||||
if (list.isNotEmpty()) {
|
|
||||||
list.reversed().forEach {
|
|
||||||
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
|
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
|
||||||
}
|
}
|
||||||
for (i in subMap) {
|
for (i in subMap) {
|
||||||
@@ -601,14 +472,9 @@ class AnilistQueries {
|
|||||||
subMap[m.id] = m
|
subMap[m.id] = m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Suppress("UNCHECKED_CAST")
|
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||||
val list = PrefManager.getNullableCustomVal(
|
if (set.isNotEmpty()) {
|
||||||
"continueAnimeList",
|
set.reversed().forEach {
|
||||||
listOf<Int>(),
|
|
||||||
List::class.java
|
|
||||||
) as List<Int>
|
|
||||||
if (list.isNotEmpty()) {
|
|
||||||
list.reversed().forEach {
|
|
||||||
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
|
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
|
||||||
}
|
}
|
||||||
for (i in subMap) {
|
for (i in subMap) {
|
||||||
@@ -692,11 +558,12 @@ class AnilistQueries {
|
|||||||
|
|
||||||
|
|
||||||
private suspend fun bannerImage(type: String): String? {
|
private suspend fun bannerImage(type: String): String? {
|
||||||
val image = BannerImage(
|
//var image = loadData<BannerImage>("banner_$type")
|
||||||
PrefManager.getCustomVal("banner_${type}_url", ""),
|
val image: BannerImage? = BannerImage(
|
||||||
|
PrefManager.getCustomVal("banner_${type}_url", null),
|
||||||
PrefManager.getCustomVal("banner_${type}_time", 0L)
|
PrefManager.getCustomVal("banner_${type}_time", 0L)
|
||||||
)
|
)
|
||||||
if (image.url.isNullOrEmpty() || image.checkTime()) {
|
if (image == null || image.checkTime()) {
|
||||||
val response =
|
val response =
|
||||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
|
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
|
||||||
val random = response?.data?.mediaListCollection?.lists?.mapNotNull {
|
val random = response?.data?.mediaListCollection?.lists?.mapNotNull {
|
||||||
@@ -725,7 +592,7 @@ class AnilistQueries {
|
|||||||
sortOrder: String? = null
|
sortOrder: String? = null
|
||||||
): MutableMap<String, ArrayList<Media>> {
|
): MutableMap<String, ArrayList<Media>> {
|
||||||
val response =
|
val response =
|
||||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage genres meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
|
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
|
||||||
val sorted = mutableMapOf<String, ArrayList<Media>>()
|
val sorted = mutableMapOf<String, ArrayList<Media>>()
|
||||||
val unsorted = mutableMapOf<String, ArrayList<Media>>()
|
val unsorted = mutableMapOf<String, ArrayList<Media>>()
|
||||||
val all = arrayListOf<Media>()
|
val all = arrayListOf<Media>()
|
||||||
@@ -753,7 +620,7 @@ class AnilistQueries {
|
|||||||
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
||||||
}
|
}
|
||||||
|
|
||||||
sorted["Favourites"] = favMedia(anime, userId)
|
sorted["Favourites"] = favMedia(anime)
|
||||||
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
||||||
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
|
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
|
||||||
sorted["Favourites"]?.forEach { fav ->
|
sorted["Favourites"]?.forEach { fav ->
|
||||||
@@ -763,7 +630,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) {
|
||||||
@@ -794,8 +661,8 @@ class AnilistQueries {
|
|||||||
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
|
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
|
||||||
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
|
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
|
||||||
mapOf(
|
mapOf(
|
||||||
true to adultTags.sortedBy { it },
|
true to adultTags,
|
||||||
false to nonAdultTags.sortedBy { it }
|
false to nonAdultTags
|
||||||
)
|
)
|
||||||
|
|
||||||
if (genres.isNullOrEmpty()) {
|
if (genres.isNullOrEmpty()) {
|
||||||
@@ -831,7 +698,7 @@ class AnilistQueries {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (!genres.isNullOrEmpty() && tags != null) {
|
return if (!genres.isNullOrEmpty() && tags != null) {
|
||||||
Anilist.genres = genres?.sortedBy { it }?.toMutableList() as ArrayList<String>
|
Anilist.genres = genres
|
||||||
Anilist.tags = tags
|
Anilist.tags = tags
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
@@ -911,23 +778,18 @@ 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, START_DATE_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]) {
|
||||||
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
|
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
total
|
total
|
||||||
@@ -972,19 +834,14 @@ 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 (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
${if (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)
|
||||||
@@ -1016,6 +873,7 @@ 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>()
|
||||||
@@ -1047,11 +905,7 @@ 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,
|
||||||
@@ -1062,156 +916,11 @@ 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) {
|
||||||
@@ -1258,26 +967,41 @@ 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) {
|
||||||
var i = 1
|
val response = execute()?.airingSchedules ?: return null
|
||||||
val list = mutableListOf<Media>()
|
val idArr = mutableListOf<Int>()
|
||||||
var res: Page? = null
|
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||||
suspend fun next() {
|
return response.mapNotNull { i ->
|
||||||
res = execute(i)
|
i.media?.let {
|
||||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
if (!idArr.contains(it.id))
|
||||||
j.media?.let {
|
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
|
||||||
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
|
idArr.add(it.id)
|
||||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
Media(it)
|
||||||
} else null
|
} else null
|
||||||
|
else null
|
||||||
}
|
}
|
||||||
} ?: listOf())
|
}.toMutableList()
|
||||||
}
|
} else {
|
||||||
next()
|
var i = 1
|
||||||
while (res?.pageInfo?.hasNextPage == true) {
|
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()
|
next()
|
||||||
i++
|
while (res?.pageInfo?.hasNextPage == true) {
|
||||||
|
next()
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return list.reversed().toMutableList()
|
||||||
}
|
}
|
||||||
return list.reversed().toMutableList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCharacterDetails(character: Character): Character {
|
suspend fun getCharacterDetails(character: Character): Character {
|
||||||
@@ -1463,39 +1187,19 @@ 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++
|
||||||
val query = executeQuery<Query.Author>(
|
hasNextPage = executeQuery<Query.Author>(
|
||||||
query(page), force = true
|
query(page),
|
||||||
)?.data?.author
|
force = true
|
||||||
hasNextPage = query?.staffMedia?.let {
|
)?.data?.author?.staffMedia?.let {
|
||||||
it.edges?.forEach { i ->
|
it.edges?.forEach { i ->
|
||||||
i.node?.apply {
|
i.node?.apply {
|
||||||
val status = status.toString()
|
val status = status.toString()
|
||||||
@@ -1510,20 +1214,6 @@ 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")) {
|
||||||
@@ -1531,155 +1221,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
|
|
||||||
return executeQuery<Query.ToggleFollow>(
|
|
||||||
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun toggleLike(id: Int, type: String): ToggleLike? {
|
|
||||||
return executeQuery<ToggleLike>(
|
|
||||||
"""mutation Like{ToggleLikeV2(id:$id,type:$type){__typename}}"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUserProfile(id: Int): Query.UserProfileResponse? {
|
|
||||||
return executeQuery<Query.UserProfileResponse>(
|
|
||||||
"""{followerPage:Page{followers(userId:$id){id}pageInfo{total}}followingPage:Page{following(userId:$id){id}pageInfo{total}}user:User(id:$id){id name about(asHtml:true)avatar{medium large}bannerImage isFollowing isFollower isBlocked favourites{anime{nodes{id coverImage{extraLarge large medium color}}}manga{nodes{id coverImage{extraLarge large medium color}}}characters{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""",
|
|
||||||
force = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUserProfile(username: String): Query.UserProfileResponse? {
|
|
||||||
val id = getUserId(username) ?: return null
|
|
||||||
return getUserProfile(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUserId(username: String): Int? {
|
|
||||||
return executeQuery<Query.User>(
|
|
||||||
"""{User(name:"$username"){id}}""",
|
|
||||||
force = true
|
|
||||||
)?.data?.user?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUserStatistics(id: Int, sort: String = "ID"): Query.StatisticsResponse? {
|
|
||||||
return executeQuery<Query.StatisticsResponse>(
|
|
||||||
"""{User(id:$id){id name mediaListOptions{scoreFormat}statistics{anime{...UserStatistics}manga{...UserStatistics}}}}fragment UserStatistics on UserStatistics{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead formats(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds format}statuses(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds status}scores(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds score}lengths(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds length}releaseYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds releaseYear}startYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds startYear}genres(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds genre}tags(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds tag{id name}}countries(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds country}voiceActors(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds voiceActor{id name{first middle last full native alternative userPreferred}}characterIds}staff(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds staff{id name{first middle last full native alternative userPreferred}}}studios(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds studio{id name isAnimationStudio}}}""",
|
|
||||||
force = true,
|
|
||||||
show = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun userFavMediaQuery(anime: Boolean, page: Int, id: Int): String {
|
|
||||||
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun userFollowing(id: Int): Query.Following? {
|
|
||||||
return executeQuery<Query.Following>(
|
|
||||||
"""{Page {following(userId:${id},sort:[USERNAME]){id name avatar{large medium}bannerImage}}}""",
|
|
||||||
force = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun userFollowers(id: Int): Query.Follower? {
|
|
||||||
return executeQuery<Query.Follower>(
|
|
||||||
"""{Page {followers(userId:${id},sort:[USERNAME]){id name avatar{large medium}bannerImage}}}""",
|
|
||||||
force = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun initProfilePage(id: Int): Query.ProfilePageMedia? {
|
|
||||||
return executeQuery<Query.ProfilePageMedia>(
|
|
||||||
"""{
|
|
||||||
favoriteAnime:${userFavMediaQuery(true, 1, id)}
|
|
||||||
favoriteManga:${userFavMediaQuery(false, 1, id)}
|
|
||||||
}""".trimIndent(), force = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun getNotifications(
|
|
||||||
id: Int,
|
|
||||||
page: Int = 1,
|
|
||||||
resetNotification: Boolean = true
|
|
||||||
): NotificationResponse? {
|
|
||||||
val reset = if (resetNotification) "true" else "false"
|
|
||||||
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,}}}}""",
|
|
||||||
force = true
|
|
||||||
)
|
|
||||||
if (res != null && resetNotification) {
|
|
||||||
val commentNotifications = PrefManager.getVal(PrefName.UnreadCommentNotifications, 0)
|
|
||||||
res.data.user.unreadNotificationCount += commentNotifications
|
|
||||||
PrefManager.setVal(PrefName.UnreadCommentNotifications, 0)
|
|
||||||
Anilist.unreadNotificationCount = 0
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getFeed(
|
|
||||||
userId: Int?,
|
|
||||||
global: Boolean = false,
|
|
||||||
page: Int = 1,
|
|
||||||
activityId: Int? = null
|
|
||||||
): FeedResponse? {
|
|
||||||
val filter = if (activityId != null) "id:$activityId,"
|
|
||||||
else if (userId != null) "userId:$userId,"
|
|
||||||
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
|
|
||||||
else "isFollowing:true,type_not:MESSAGE,"
|
|
||||||
return executeQuery<FeedResponse>(
|
|
||||||
"""{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id title{english romaji native userPreferred}bannerImage coverImage{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id recipientId messengerId type replyCount likeCount message(asHtml:true)isLocked isSubscribed isLiked isPrivate siteUrl createdAt recipient{id name bannerImage avatar{medium large}}messenger{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}}}}""",
|
|
||||||
force = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUpcomingAnime(id: String): List<Media> {
|
|
||||||
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) {
|
|
||||||
AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id }
|
|
||||||
?: false
|
|
||||||
|
|
||||||
AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id }
|
|
||||||
?: 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ITEMS_PER_PAGE = 25
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,6 @@ 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 kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -58,40 +57,48 @@ 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()
|
||||||
Logger.log("AnilistHomeViewModel : res=$res")
|
|
||||||
res["currentAnime"]?.let { animeContinue.postValue(it) }
|
res["currentAnime"]?.let { animeContinue.postValue(it) }
|
||||||
res["favoriteAnime"]?.let { animeFav.postValue(it) }
|
res["favoriteAnime"]?.let { animeFav.postValue(it) }
|
||||||
res["plannedAnime"]?.let { animePlanned.postValue(it) }
|
res["plannedAnime"]?.let { animePlanned.postValue(it) }
|
||||||
@@ -103,8 +110,8 @@ class AnilistHomeViewModel : ViewModel() {
|
|||||||
|
|
||||||
suspend fun loadMain(context: FragmentActivity) {
|
suspend fun loadMain(context: FragmentActivity) {
|
||||||
Anilist.getSavedToken()
|
Anilist.getSavedToken()
|
||||||
MAL.getSavedToken()
|
MAL.getSavedToken(context)
|
||||||
Discord.getSavedToken()
|
Discord.getSavedToken(context)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -135,19 +142,22 @@ 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,
|
||||||
searchVal: String? = null,
|
search_val: 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,
|
||||||
@@ -155,11 +165,10 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||||||
animePopular.postValue(
|
animePopular.postValue(
|
||||||
Anilist.query.search(
|
Anilist.query.search(
|
||||||
type,
|
type,
|
||||||
search = searchVal,
|
search = search_val,
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,43 +183,13 @@ 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() {
|
||||||
@@ -228,17 +207,29 @@ 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,
|
||||||
searchVal: String? = null,
|
search_val: 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,
|
||||||
@@ -246,11 +237,10 @@ class AnilistMangaViewModel : ViewModel() {
|
|||||||
mangaPopular.postValue(
|
mangaPopular.postValue(
|
||||||
Anilist.query.search(
|
Anilist.query.search(
|
||||||
type,
|
type,
|
||||||
search = searchVal,
|
search = search_val,
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -265,55 +255,17 @@ 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() {
|
||||||
@@ -332,17 +284,13 @@ 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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -355,15 +303,11 @@ 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
|
||||||
)
|
)
|
||||||
@@ -387,40 +331,4 @@ class GenresViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class ProfileViewModel : ViewModel() {
|
|
||||||
|
|
||||||
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
|
||||||
MutableLiveData<ArrayList<Media>>(null)
|
|
||||||
|
|
||||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
|
||||||
|
|
||||||
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
|
||||||
MutableLiveData<ArrayList<Media>>(null)
|
|
||||||
|
|
||||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
|
||||||
|
|
||||||
suspend fun setData(id: Int) {
|
|
||||||
val res = Anilist.query.initProfilePage(id)
|
|
||||||
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
|
|
||||||
it.node?.let { i ->
|
|
||||||
Media(i).apply { isFav = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mangaFav.postValue(ArrayList(mangaList ?: arrayListOf()))
|
|
||||||
val animeList = res?.data?.favoriteAnime?.favourites?.anime?.edges?.mapNotNull {
|
|
||||||
it.node?.let { i ->
|
|
||||||
Media(i).apply { isFav = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
mangaFav.postValue(mangaFav.value)
|
|
||||||
animeFav.postValue(animeFav.value)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.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
|
||||||
@@ -15,6 +16,7 @@ class Login : AppCompatActivity() {
|
|||||||
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
val data: Uri? = intent?.data
|
val data: Uri? = intent?.data
|
||||||
|
logger(data.toString())
|
||||||
try {
|
try {
|
||||||
Anilist.token =
|
Anilist.token =
|
||||||
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||||
|
|||||||
@@ -11,17 +11,13 @@ 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>,
|
||||||
@@ -41,24 +37,12 @@ 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()))
|
||||||
}
|
}
|
||||||
@@ -90,12 +74,8 @@ 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)
|
||||||
|
|||||||
@@ -11,25 +11,20 @@ import ani.dantotsu.themes.ThemeManager
|
|||||||
class UrlMedia : Activity() {
|
class UrlMedia : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
val data: Uri? = intent?.data
|
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||||
val type = data?.pathSegments?.getOrNull(0)
|
var isMAL = false
|
||||||
if (type != "user") {
|
var continueMedia = true
|
||||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
if (id == 0) {
|
||||||
var isMAL = false
|
continueMedia = false
|
||||||
var continueMedia = true
|
val data: Uri? = intent?.data
|
||||||
if (id == 0) {
|
isMAL = data?.host != "anilist.co"
|
||||||
continueMedia = false
|
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||||
isMAL = data?.host != "anilist.co"
|
} else loadMedia = id
|
||||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
startMainActivity(
|
||||||
} else loadMedia = id
|
this,
|
||||||
startMainActivity(
|
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
|
||||||
this,
|
)
|
||||||
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val username = data.pathSegments?.getOrNull(1)
|
|
||||||
startMainActivity(this, bundleOf("username" to username))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ data class Character(
|
|||||||
|
|
||||||
// Notes for site moderators
|
// Notes for site moderators
|
||||||
@SerialName("modNotes") var modNotes: String?,
|
@SerialName("modNotes") var modNotes: String?,
|
||||||
) : java.io.Serializable
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CharacterConnection(
|
data class CharacterConnection(
|
||||||
@@ -55,8 +55,8 @@ 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
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CharacterEdge(
|
data class CharacterEdge(
|
||||||
@@ -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>?,
|
||||||
@@ -82,7 +82,7 @@ data class CharacterEdge(
|
|||||||
|
|
||||||
// The order the character should be displayed from the users favourites
|
// The order the character should be displayed from the users favourites
|
||||||
@SerialName("favouriteOrder") var favouriteOrder: Int?,
|
@SerialName("favouriteOrder") var favouriteOrder: Int?,
|
||||||
) : java.io.Serializable
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CharacterName(
|
data class CharacterName(
|
||||||
@@ -109,7 +109,7 @@ data class CharacterName(
|
|||||||
|
|
||||||
// The currently authenticated users preferred name language. Default romaji for non-authenticated
|
// The currently authenticated users preferred name language. Default romaji for non-authenticated
|
||||||
@SerialName("userPreferred") var userPreferred: String?,
|
@SerialName("userPreferred") var userPreferred: String?,
|
||||||
) : java.io.Serializable
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CharacterImage(
|
data class CharacterImage(
|
||||||
@@ -118,4 +118,4 @@ data class CharacterImage(
|
|||||||
|
|
||||||
// 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
|
)
|
||||||
@@ -24,9 +24,7 @@ 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?
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,586 +139,41 @@ class Query {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ProfilePageMedia(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data?
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
|
|
||||||
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@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?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ToggleFollow(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data?
|
|
||||||
) : java.io.Serializable {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("ToggleFollow")
|
|
||||||
val toggleFollow: FollowData
|
|
||||||
) : java.io.Serializable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GenreCollection(
|
data class GenreCollection(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
) : java.io.Serializable {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("GenreCollection")
|
@SerialName("GenreCollection")
|
||||||
val genreCollection: List<String>?
|
val genreCollection: List<String>?
|
||||||
) : java.io.Serializable
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MediaTagCollection(
|
data class MediaTagCollection(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
) : java.io.Serializable {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("MediaTagCollection")
|
@SerialName("MediaTagCollection")
|
||||||
val mediaTagCollection: List<MediaTag>?
|
val mediaTagCollection: List<MediaTag>?
|
||||||
) : java.io.Serializable
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class User(
|
data class User(
|
||||||
@SerialName("data")
|
@SerialName("data")
|
||||||
val data: Data
|
val data: Data
|
||||||
) : java.io.Serializable {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
@SerialName("User")
|
@SerialName("User")
|
||||||
val user: ani.dantotsu.connections.anilist.api.User?
|
val user: ani.dantotsu.connections.anilist.api.User?
|
||||||
) : java.io.Serializable
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserProfileResponse(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data
|
|
||||||
) : java.io.Serializable {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("followerPage")
|
|
||||||
val followerPage: UserProfilePage?,
|
|
||||||
@SerialName("followingPage")
|
|
||||||
val followingPage: UserProfilePage?,
|
|
||||||
@SerialName("user")
|
|
||||||
val user: UserProfile?
|
|
||||||
) : java.io.Serializable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserProfilePage(
|
|
||||||
@SerialName("pageInfo")
|
|
||||||
val pageInfo: PageInfo,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Following(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data
|
|
||||||
) : java.io.Serializable {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("Page")
|
|
||||||
val page: FollowingPage?
|
|
||||||
) : java.io.Serializable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Follower(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data
|
|
||||||
) : java.io.Serializable {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("Page")
|
|
||||||
val page: FollowerPage?
|
|
||||||
) : java.io.Serializable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FollowerPage(
|
|
||||||
@SerialName("followers")
|
|
||||||
val followers: List<ani.dantotsu.connections.anilist.api.User>?
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FollowingPage(
|
|
||||||
@SerialName("following")
|
|
||||||
val following: List<ani.dantotsu.connections.anilist.api.User>?
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserProfile(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("name")
|
|
||||||
val name: String,
|
|
||||||
@SerialName("about")
|
|
||||||
val about: String?,
|
|
||||||
@SerialName("avatar")
|
|
||||||
val avatar: UserAvatar?,
|
|
||||||
@SerialName("bannerImage")
|
|
||||||
val bannerImage: String?,
|
|
||||||
@SerialName("isFollowing")
|
|
||||||
var isFollowing: Boolean,
|
|
||||||
@SerialName("isFollower")
|
|
||||||
val isFollower: Boolean,
|
|
||||||
@SerialName("isBlocked")
|
|
||||||
val isBlocked: Boolean,
|
|
||||||
@SerialName("favourites")
|
|
||||||
val favourites: UserFavourites?,
|
|
||||||
@SerialName("statistics")
|
|
||||||
val statistics: NNUserStatisticTypes,
|
|
||||||
@SerialName("siteUrl")
|
|
||||||
val siteUrl: String,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class NNUserStatisticTypes(
|
|
||||||
@SerialName("anime") var anime: NNUserStatistics,
|
|
||||||
@SerialName("manga") var manga: NNUserStatistics
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class NNUserStatistics(
|
|
||||||
@SerialName("count") var count: Int,
|
|
||||||
@SerialName("meanScore") var meanScore: Float,
|
|
||||||
@SerialName("standardDeviation") var standardDeviation: Float,
|
|
||||||
@SerialName("minutesWatched") var minutesWatched: Int,
|
|
||||||
@SerialName("episodesWatched") var episodesWatched: Int,
|
|
||||||
@SerialName("chaptersRead") var chaptersRead: Int,
|
|
||||||
@SerialName("volumesRead") var volumesRead: Int,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserFavourites(
|
|
||||||
@SerialName("anime")
|
|
||||||
val anime: UserMediaFavouritesCollection,
|
|
||||||
@SerialName("manga")
|
|
||||||
val manga: UserMediaFavouritesCollection,
|
|
||||||
@SerialName("characters")
|
|
||||||
val characters: UserCharacterFavouritesCollection,
|
|
||||||
@SerialName("staff")
|
|
||||||
val staff: UserStaffFavouritesCollection,
|
|
||||||
@SerialName("studios")
|
|
||||||
val studios: UserStudioFavouritesCollection,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserMediaFavouritesCollection(
|
|
||||||
@SerialName("nodes")
|
|
||||||
val nodes: List<UserMediaImageFavorite>,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserMediaImageFavorite(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("coverImage")
|
|
||||||
val coverImage: MediaCoverImage
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserCharacterFavouritesCollection(
|
|
||||||
@SerialName("nodes")
|
|
||||||
val nodes: List<UserCharacterImageFavorite>,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserCharacterImageFavorite(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("name")
|
|
||||||
val name: CharacterName,
|
|
||||||
@SerialName("image")
|
|
||||||
val image: CharacterImage,
|
|
||||||
@SerialName("isFavourite")
|
|
||||||
val isFavourite: Boolean
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserStaffFavouritesCollection(
|
|
||||||
@SerialName("nodes")
|
|
||||||
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserStudioFavouritesCollection(
|
|
||||||
@SerialName("nodes")
|
|
||||||
val nodes: List<UserStudioFavorite>,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserStudioFavorite(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("name")
|
|
||||||
val name: String,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
//----------------------------------------
|
|
||||||
// Statistics
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsResponse(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data
|
|
||||||
) : java.io.Serializable {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("User")
|
|
||||||
val user: StatisticsUser?
|
|
||||||
) : java.io.Serializable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsUser(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("name")
|
|
||||||
val name: String,
|
|
||||||
@SerialName("mediaListOptions")
|
|
||||||
val mediaListOptions: MediaListOptions,
|
|
||||||
@SerialName("statistics")
|
|
||||||
val statistics: StatisticsTypes
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsTypes(
|
|
||||||
@SerialName("anime")
|
|
||||||
val anime: Statistics,
|
|
||||||
@SerialName("manga")
|
|
||||||
val manga: Statistics
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Statistics(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("standardDeviation")
|
|
||||||
val standardDeviation: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("episodesWatched")
|
|
||||||
val episodesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("volumesRead")
|
|
||||||
val volumesRead: Int,
|
|
||||||
@SerialName("formats")
|
|
||||||
val formats: List<StatisticsFormat>,
|
|
||||||
@SerialName("statuses")
|
|
||||||
val statuses: List<StatisticsStatus>,
|
|
||||||
@SerialName("scores")
|
|
||||||
val scores: List<StatisticsScore>,
|
|
||||||
@SerialName("lengths")
|
|
||||||
val lengths: List<StatisticsLength>,
|
|
||||||
@SerialName("releaseYears")
|
|
||||||
val releaseYears: List<StatisticsReleaseYear>,
|
|
||||||
@SerialName("startYears")
|
|
||||||
val startYears: List<StatisticsStartYear>,
|
|
||||||
@SerialName("genres")
|
|
||||||
val genres: List<StatisticsGenre>,
|
|
||||||
@SerialName("tags")
|
|
||||||
val tags: List<StatisticsTag>,
|
|
||||||
@SerialName("countries")
|
|
||||||
val countries: List<StatisticsCountry>,
|
|
||||||
@SerialName("voiceActors")
|
|
||||||
val voiceActors: List<StatisticsVoiceActor>,
|
|
||||||
@SerialName("staff")
|
|
||||||
val staff: List<StatisticsStaff>,
|
|
||||||
@SerialName("studios")
|
|
||||||
val studios: List<StatisticsStudio>
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsFormat(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("format")
|
|
||||||
val format: String
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsStatus(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("status")
|
|
||||||
val status: String
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsScore(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("score")
|
|
||||||
val score: Int
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsLength(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("length")
|
|
||||||
val length: String? //can be null for manga
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsReleaseYear(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("releaseYear")
|
|
||||||
val releaseYear: Int
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsStartYear(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("startYear")
|
|
||||||
val startYear: Int
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsGenre(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("genre")
|
|
||||||
val genre: String
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsTag(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("tag")
|
|
||||||
val tag: Tag
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Tag(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("name")
|
|
||||||
val name: String
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsCountry(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("country")
|
|
||||||
val country: String
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsVoiceActor(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("voiceActor")
|
|
||||||
val voiceActor: VoiceActor,
|
|
||||||
@SerialName("characterIds")
|
|
||||||
val characterIds: List<Int>
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class VoiceActor(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("name")
|
|
||||||
val name: StaffName
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StaffName(
|
|
||||||
@SerialName("first")
|
|
||||||
val first: String?,
|
|
||||||
@SerialName("middle")
|
|
||||||
val middle: String?,
|
|
||||||
@SerialName("last")
|
|
||||||
val last: String?,
|
|
||||||
@SerialName("full")
|
|
||||||
val full: String?,
|
|
||||||
@SerialName("native")
|
|
||||||
val native: String?,
|
|
||||||
@SerialName("alternative")
|
|
||||||
val alternative: List<String>?,
|
|
||||||
@SerialName("userPreferred")
|
|
||||||
val userPreferred: String?
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsStaff(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("staff")
|
|
||||||
val staff: VoiceActor
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatisticsStudio(
|
|
||||||
@SerialName("count")
|
|
||||||
val count: Int,
|
|
||||||
@SerialName("meanScore")
|
|
||||||
val meanScore: Float,
|
|
||||||
@SerialName("minutesWatched")
|
|
||||||
val minutesWatched: Int,
|
|
||||||
@SerialName("chaptersRead")
|
|
||||||
val chaptersRead: Int,
|
|
||||||
@SerialName("mediaIds")
|
|
||||||
val mediaIds: List<Int>,
|
|
||||||
@SerialName("studio")
|
|
||||||
val studio: StatStudio
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StatStudio(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("name")
|
|
||||||
val name: String,
|
|
||||||
@SerialName("isAnimationStudio")
|
|
||||||
val isAnimationStudio: Boolean
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//data class WhaData(
|
//data class WhaData(
|
||||||
@@ -750,7 +203,7 @@ class Query {
|
|||||||
// // Activity reply query
|
// // Activity reply query
|
||||||
// val ActivityReply: ActivityReply?,
|
// val ActivityReply: ActivityReply?,
|
||||||
|
|
||||||
// // CommentNotificationWorker query
|
// // Comment query
|
||||||
// val ThreadComment: List<ThreadComment>?,
|
// val ThreadComment: List<ThreadComment>?,
|
||||||
|
|
||||||
// // Notification query
|
// // Notification query
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
package ani.dantotsu.connections.anilist.api
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FeedResponse(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data
|
|
||||||
) : java.io.Serializable {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("Page")
|
|
||||||
val page: ActivityPage
|
|
||||||
) : java.io.Serializable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ActivityPage(
|
|
||||||
@SerialName("activities")
|
|
||||||
val activities: List<Activity>
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Activity(
|
|
||||||
@SerialName("__typename")
|
|
||||||
val typename: String,
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("recipientId")
|
|
||||||
val recipientId: Int?,
|
|
||||||
@SerialName("messengerId")
|
|
||||||
val messengerId: Int?,
|
|
||||||
@SerialName("userId")
|
|
||||||
val userId: Int?,
|
|
||||||
@SerialName("type")
|
|
||||||
val type: String,
|
|
||||||
@SerialName("replyCount")
|
|
||||||
val replyCount: Int,
|
|
||||||
@SerialName("status")
|
|
||||||
val status: String?,
|
|
||||||
@SerialName("progress")
|
|
||||||
val progress: String?,
|
|
||||||
@SerialName("text")
|
|
||||||
val text: String?,
|
|
||||||
@SerialName("message")
|
|
||||||
val message: String?,
|
|
||||||
@SerialName("siteUrl")
|
|
||||||
val siteUrl: String?,
|
|
||||||
@SerialName("isLocked")
|
|
||||||
val isLocked: Boolean,
|
|
||||||
@SerialName("isSubscribed")
|
|
||||||
val isSubscribed: Boolean,
|
|
||||||
@SerialName("likeCount")
|
|
||||||
var likeCount: Int?,
|
|
||||||
@SerialName("isLiked")
|
|
||||||
var isLiked: Boolean?,
|
|
||||||
@SerialName("isPinned")
|
|
||||||
val isPinned: Boolean?,
|
|
||||||
@SerialName("isPrivate")
|
|
||||||
val isPrivate: Boolean?,
|
|
||||||
@SerialName("createdAt")
|
|
||||||
val createdAt: Int,
|
|
||||||
@SerialName("user")
|
|
||||||
val user: User?,
|
|
||||||
@SerialName("recipient")
|
|
||||||
val recipient: User?,
|
|
||||||
@SerialName("messenger")
|
|
||||||
val messenger: User?,
|
|
||||||
@SerialName("media")
|
|
||||||
val media: Media?,
|
|
||||||
@SerialName("replies")
|
|
||||||
val replies: List<ActivityReply>?,
|
|
||||||
@SerialName("likes")
|
|
||||||
val likes: List<User>?,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ActivityReply(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("userId")
|
|
||||||
val userId: Int,
|
|
||||||
@SerialName("text")
|
|
||||||
val text: String,
|
|
||||||
@SerialName("likeCount")
|
|
||||||
val likeCount: Int,
|
|
||||||
@SerialName("isLiked")
|
|
||||||
val isLiked: Boolean,
|
|
||||||
@SerialName("createdAt")
|
|
||||||
val createdAt: Int,
|
|
||||||
@SerialName("user")
|
|
||||||
val user: User,
|
|
||||||
@SerialName("likes")
|
|
||||||
val likes: List<User>?,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ToggleLike(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data
|
|
||||||
) : java.io.Serializable {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("ToggleLikeV2")
|
|
||||||
val toggleLike: LikeData
|
|
||||||
) : java.io.Serializable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class LikeData(
|
|
||||||
@SerialName("__typename")
|
|
||||||
val typename: String
|
|
||||||
) : java.io.Serializable
|
|
||||||
@@ -251,7 +251,7 @@ data class MediaCoverImage(
|
|||||||
|
|
||||||
// Average #hex color of cover image
|
// Average #hex color of cover image
|
||||||
@SerialName("color") var color: String?,
|
@SerialName("color") var color: String?,
|
||||||
) : java.io.Serializable
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MediaList(
|
data class MediaList(
|
||||||
@@ -490,7 +490,7 @@ data class MediaExternalLink(
|
|||||||
|
|
||||||
// isDisabled: Boolean
|
// isDisabled: Boolean
|
||||||
@SerialName("notes") var notes: String?,
|
@SerialName("notes") var notes: String?,
|
||||||
) : java.io.Serializable
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class ExternalLinkType {
|
enum class ExternalLinkType {
|
||||||
@@ -512,13 +512,7 @@ data class MediaListCollection(
|
|||||||
// If there is another chunk
|
// If there is another chunk
|
||||||
@SerialName("hasNextChunk") var hasNextChunk: Boolean?,
|
@SerialName("hasNextChunk") var hasNextChunk: Boolean?,
|
||||||
|
|
||||||
) : java.io.Serializable
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FollowData(
|
|
||||||
@SerialName("id") var id: Int,
|
|
||||||
@SerialName("isFollowing") var isFollowing: Boolean,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MediaListGroup(
|
data class MediaListGroup(
|
||||||
@@ -532,4 +526,4 @@ data class MediaListGroup(
|
|||||||
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
|
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
|
||||||
|
|
||||||
@SerialName("status") var status: MediaListStatus?,
|
@SerialName("status") var status: MediaListStatus?,
|
||||||
) : java.io.Serializable
|
)
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package ani.dantotsu.connections.anilist.api
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
enum class NotificationType(val value: String) {
|
|
||||||
ACTIVITY_MESSAGE("ACTIVITY_MESSAGE"),
|
|
||||||
ACTIVITY_REPLY("ACTIVITY_REPLY"),
|
|
||||||
FOLLOWING("FOLLOWING"),
|
|
||||||
ACTIVITY_MENTION("ACTIVITY_MENTION"),
|
|
||||||
THREAD_COMMENT_MENTION("THREAD_COMMENT_MENTION"),
|
|
||||||
THREAD_SUBSCRIBED("THREAD_SUBSCRIBED"),
|
|
||||||
THREAD_COMMENT_REPLY("THREAD_COMMENT_REPLY"),
|
|
||||||
AIRING("AIRING"),
|
|
||||||
ACTIVITY_LIKE("ACTIVITY_LIKE"),
|
|
||||||
ACTIVITY_REPLY_LIKE("ACTIVITY_REPLY_LIKE"),
|
|
||||||
THREAD_LIKE("THREAD_LIKE"),
|
|
||||||
THREAD_COMMENT_LIKE("THREAD_COMMENT_LIKE"),
|
|
||||||
ACTIVITY_REPLY_SUBSCRIBED("ACTIVITY_REPLY_SUBSCRIBED"),
|
|
||||||
RELATED_MEDIA_ADDITION("RELATED_MEDIA_ADDITION"),
|
|
||||||
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
|
|
||||||
MEDIA_MERGE("MEDIA_MERGE"),
|
|
||||||
MEDIA_DELETION("MEDIA_DELETION"),
|
|
||||||
|
|
||||||
//custom
|
|
||||||
COMMENT_REPLY("COMMENT_REPLY"),
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class NotificationResponse(
|
|
||||||
@SerialName("data")
|
|
||||||
val data: Data,
|
|
||||||
) : java.io.Serializable {
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
@SerialName("User")
|
|
||||||
val user: NotificationUser,
|
|
||||||
@SerialName("Page")
|
|
||||||
val page: NotificationPage,
|
|
||||||
) : java.io.Serializable
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class NotificationUser(
|
|
||||||
@SerialName("unreadNotificationCount")
|
|
||||||
var unreadNotificationCount: Int,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class NotificationPage(
|
|
||||||
@SerialName("pageInfo")
|
|
||||||
val pageInfo: PageInfo,
|
|
||||||
@SerialName("notifications")
|
|
||||||
val notifications: List<Notification>,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Notification(
|
|
||||||
@SerialName("__typename")
|
|
||||||
val typename: String,
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("userId")
|
|
||||||
val userId: Int? = null,
|
|
||||||
@SerialName("CommentId")
|
|
||||||
val commentId: Int?,
|
|
||||||
@SerialName("type")
|
|
||||||
val notificationType: String,
|
|
||||||
@SerialName("activityId")
|
|
||||||
val activityId: Int? = null,
|
|
||||||
@SerialName("animeId")
|
|
||||||
val mediaId: Int? = null,
|
|
||||||
@SerialName("episode")
|
|
||||||
val episode: Int? = null,
|
|
||||||
@SerialName("contexts")
|
|
||||||
val contexts: List<String>? = null,
|
|
||||||
@SerialName("context")
|
|
||||||
val context: String? = null,
|
|
||||||
@SerialName("reason")
|
|
||||||
val reason: String? = null,
|
|
||||||
@SerialName("deletedMediaTitle")
|
|
||||||
val deletedMediaTitle: String? = null,
|
|
||||||
@SerialName("deletedMediaTitles")
|
|
||||||
val deletedMediaTitles: List<String>? = null,
|
|
||||||
@SerialName("createdAt")
|
|
||||||
val createdAt: Int,
|
|
||||||
@SerialName("media")
|
|
||||||
val media: Media? = null,
|
|
||||||
@SerialName("user")
|
|
||||||
val user: User? = null,
|
|
||||||
@SerialName("message")
|
|
||||||
val message: MessageActivity? = null,
|
|
||||||
@SerialName("activity")
|
|
||||||
val activity: ActivityUnion? = null,
|
|
||||||
@SerialName("Thread")
|
|
||||||
val thread: Thread? = null,
|
|
||||||
@SerialName("comment")
|
|
||||||
val comment: ThreadComment? = null,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MessageActivity(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int?,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ActivityUnion(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int?,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Thread(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int?,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ThreadComment(
|
|
||||||
@SerialName("id")
|
|
||||||
val id: Int?,
|
|
||||||
) : java.io.Serializable
|
|
||||||
@@ -15,7 +15,7 @@ data class Staff(
|
|||||||
@SerialName("languageV2") var languageV2: String?,
|
@SerialName("languageV2") var languageV2: String?,
|
||||||
|
|
||||||
// The staff images
|
// The staff images
|
||||||
@SerialName("image") var image: StaffImage?,
|
// @SerialName("image") var image: StaffImage?,
|
||||||
|
|
||||||
// A general description of the staff member
|
// A general description of the staff member
|
||||||
@SerialName("description") var description: String?,
|
@SerialName("description") var description: String?,
|
||||||
@@ -94,15 +94,6 @@ data class StaffConnection(
|
|||||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StaffImage(
|
|
||||||
// The character's image of media at its largest size
|
|
||||||
@SerialName("large") var large: String?,
|
|
||||||
|
|
||||||
// The character's image of media at medium size
|
|
||||||
@SerialName("medium") var medium: String?,
|
|
||||||
) : java.io.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StaffEdge(
|
data class StaffEdge(
|
||||||
var role: String?,
|
var role: String?,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ data class User(
|
|||||||
@SerialName("statistics") var statistics: UserStatisticTypes?,
|
@SerialName("statistics") var statistics: UserStatisticTypes?,
|
||||||
|
|
||||||
// The number of unread notifications the user has
|
// The number of unread notifications the user has
|
||||||
@SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
|
// @SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
|
||||||
|
|
||||||
// The url for the user page on the AniList website
|
// The url for the user page on the AniList website
|
||||||
// @SerialName("siteUrl") var siteUrl: String?,
|
// @SerialName("siteUrl") var siteUrl: 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
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserStatisticTypes(
|
data class UserStatisticTypes(
|
||||||
@@ -164,7 +164,7 @@ data class Favourites(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MediaListOptions(
|
data class MediaListOptions(
|
||||||
// The score format the user is using for media lists
|
// The score format the user is using for media lists
|
||||||
@SerialName("scoreFormat") var scoreFormat: String?,
|
// @SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
|
||||||
|
|
||||||
// The default order list rows should be displayed in
|
// The default order list rows should be displayed in
|
||||||
@SerialName("rowOrder") var rowOrder: String?,
|
@SerialName("rowOrder") var rowOrder: String?,
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,570 +0,0 @@
|
|||||||
package ani.dantotsu.connections.comments
|
|
||||||
|
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
|
||||||
import ani.dantotsu.snackString
|
|
||||||
import ani.dantotsu.toast
|
|
||||||
import com.lagradost.nicehttp.NiceResponse
|
|
||||||
import com.lagradost.nicehttp.Requests
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okio.IOException
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
object CommentsAPI {
|
|
||||||
private const val ADDRESS: String = "https://1224665.xyz:443"
|
|
||||||
var authToken: String? = null
|
|
||||||
var userId: String? = null
|
|
||||||
var isBanned: Boolean = false
|
|
||||||
var isAdmin: Boolean = false
|
|
||||||
var isMod: Boolean = false
|
|
||||||
var totalVotes: Int = 0
|
|
||||||
|
|
||||||
suspend fun getCommentsForId(
|
|
||||||
id: Int,
|
|
||||||
page: Int = 1,
|
|
||||||
tag: Int?,
|
|
||||||
sort: String?
|
|
||||||
): CommentResponse? {
|
|
||||||
var url = "$ADDRESS/comments/$id/$page"
|
|
||||||
val request = requestBuilder()
|
|
||||||
tag?.let {
|
|
||||||
url += "?tag=$it"
|
|
||||||
}
|
|
||||||
sort?.let {
|
|
||||||
url += if (tag != null) "&sort=$it" else "?sort=$it"
|
|
||||||
}
|
|
||||||
val json = try {
|
|
||||||
request.get(url)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to fetch comments")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!json.text.startsWith("{")) return null
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res && json.code != 404) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
}
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<CommentResponse>(json.text)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
|
|
||||||
val url = "$ADDRESS/comments/parent/$id/$page"
|
|
||||||
val request = requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.get(url)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to fetch comments")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!json.text.startsWith("{")) return null
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res && json.code != 404) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
}
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<CommentResponse>(json.text)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getSingleComment(id: Int): Comment? {
|
|
||||||
val url = "$ADDRESS/comments/$id"
|
|
||||||
val request = requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.get(url)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to fetch comment")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!json.text.startsWith("{")) return null
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res && json.code != 404) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
}
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<Comment>(json.text)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun vote(commentId: Int, voteType: Int): Boolean {
|
|
||||||
val url = "$ADDRESS/comments/vote/$commentId/$voteType"
|
|
||||||
val request = requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.post(url)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to vote")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
|
|
||||||
val url = "$ADDRESS/comments"
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("user_id", userId ?: return null)
|
|
||||||
.add("media_id", mediaId.toString())
|
|
||||||
.add("content", content)
|
|
||||||
if (tag != null) {
|
|
||||||
body.add("tag", tag.toString())
|
|
||||||
}
|
|
||||||
parentCommentId?.let {
|
|
||||||
body.add("parent_comment_id", it.toString())
|
|
||||||
}
|
|
||||||
val request = requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.post(url, requestBody = body.build())
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to comment")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<ReturnedComment>(json.text)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
snackString("Failed to parse comment")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return Comment(
|
|
||||||
parsed.id,
|
|
||||||
parsed.userId,
|
|
||||||
parsed.mediaId,
|
|
||||||
parsed.parentCommentId,
|
|
||||||
parsed.content,
|
|
||||||
parsed.timestamp,
|
|
||||||
parsed.deleted,
|
|
||||||
parsed.tag,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
Anilist.username ?: "",
|
|
||||||
Anilist.avatar,
|
|
||||||
totalVotes = totalVotes
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteComment(commentId: Int): Boolean {
|
|
||||||
val url = "$ADDRESS/comments/$commentId"
|
|
||||||
val request = requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.delete(url)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to delete comment")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun editComment(commentId: Int, content: String): Boolean {
|
|
||||||
val url = "$ADDRESS/comments/$commentId"
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("content", content)
|
|
||||||
.build()
|
|
||||||
val request = requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.put(url, requestBody = body)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to edit comment")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun banUser(userId: String): Boolean {
|
|
||||||
val url = "$ADDRESS/ban/$userId"
|
|
||||||
val request = requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.post(url)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to ban user")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun reportComment(
|
|
||||||
commentId: Int,
|
|
||||||
username: String,
|
|
||||||
mediaTitle: String,
|
|
||||||
reportedId: String
|
|
||||||
): Boolean {
|
|
||||||
val url = "$ADDRESS/report/$commentId"
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("username", username)
|
|
||||||
.add("mediaName", mediaTitle)
|
|
||||||
.add("reporter", Anilist.username ?: "unknown")
|
|
||||||
.add("reportedId", reportedId)
|
|
||||||
.build()
|
|
||||||
val request = requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.post(url, requestBody = body)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to report comment")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
|
|
||||||
val url = "$ADDRESS/notification/reply"
|
|
||||||
val request = requestBuilder(client)
|
|
||||||
val json = try {
|
|
||||||
request.get(url)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!json.text.startsWith("{")) return null
|
|
||||||
val res = json.code == 200
|
|
||||||
if (!res) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<NotificationResponse>(json.text)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
|
|
||||||
val url = "$ADDRESS/user"
|
|
||||||
val request = if (client != null) requestBuilder(client) else requestBuilder()
|
|
||||||
val json = try {
|
|
||||||
request.get(url)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (json.code == 200) {
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<UserResponse>(json.text)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
isBanned = parsed.user.isBanned ?: false
|
|
||||||
isAdmin = parsed.user.isAdmin ?: false
|
|
||||||
isMod = parsed.user.isMod ?: false
|
|
||||||
totalVotes = parsed.user.totalVotes
|
|
||||||
return parsed.user
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchAuthToken(client: OkHttpClient? = null) {
|
|
||||||
if (authToken != null) return
|
|
||||||
val MAX_RETRIES = 5
|
|
||||||
val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days
|
|
||||||
val tokenExpiry = PrefManager.getVal<Long>(PrefName.CommentTokenExpiry)
|
|
||||||
if (tokenExpiry < System.currentTimeMillis() + tokenLifetime) {
|
|
||||||
val commentResponse =
|
|
||||||
PrefManager.getNullableVal<AuthResponse>(PrefName.CommentAuthResponse, null)
|
|
||||||
if (commentResponse != null) {
|
|
||||||
authToken = commentResponse.authToken
|
|
||||||
userId = commentResponse.user.id
|
|
||||||
isBanned = commentResponse.user.isBanned ?: false
|
|
||||||
isAdmin = commentResponse.user.isAdmin ?: false
|
|
||||||
isMod = commentResponse.user.isMod ?: false
|
|
||||||
totalVotes = commentResponse.user.totalVotes
|
|
||||||
if (getUserDetails(client) != null) return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
val url = "$ADDRESS/authenticate"
|
|
||||||
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
|
|
||||||
repeat(MAX_RETRIES) {
|
|
||||||
try {
|
|
||||||
val json = authRequest(token, url, client)
|
|
||||||
if (json.code == 200) {
|
|
||||||
if (!json.text.startsWith("{")) throw IOException("Invalid response")
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<AuthResponse>(json.text)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
snackString("Failed to login to comments API: ${e.printStackTrace()}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PrefManager.setVal(PrefName.CommentAuthResponse, parsed)
|
|
||||||
PrefManager.setVal(
|
|
||||||
PrefName.CommentTokenExpiry,
|
|
||||||
System.currentTimeMillis() + tokenLifetime
|
|
||||||
)
|
|
||||||
authToken = parsed.authToken
|
|
||||||
userId = parsed.user.id
|
|
||||||
isBanned = parsed.user.isBanned ?: false
|
|
||||||
isAdmin = parsed.user.isAdmin ?: false
|
|
||||||
isMod = parsed.user.isMod ?: false
|
|
||||||
totalVotes = parsed.user.totalVotes
|
|
||||||
return
|
|
||||||
} else if (json.code != 429) {
|
|
||||||
errorReason(json.code, json.text)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
snackString("Failed to login to comments API")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
kotlinx.coroutines.delay(60000)
|
|
||||||
}
|
|
||||||
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(
|
|
||||||
token: String,
|
|
||||||
url: String,
|
|
||||||
client: OkHttpClient? = null
|
|
||||||
): NiceResponse {
|
|
||||||
val body: FormBody = FormBody.Builder()
|
|
||||||
.add("token", token)
|
|
||||||
.build()
|
|
||||||
val request = if (client != null) requestBuilder(client) else requestBuilder()
|
|
||||||
return request.post(url, requestBody = body)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun headerBuilder(): Map<String, String> {
|
|
||||||
val map = mutableMapOf(
|
|
||||||
"appauth" to "6*45Qp%W2RS@t38jkXoSKY588Ynj%n"
|
|
||||||
)
|
|
||||||
if (authToken != null) {
|
|
||||||
map["Authorization"] = authToken!!
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
|
|
||||||
return Requests(
|
|
||||||
client,
|
|
||||||
headerBuilder()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun errorReason(code: Int, reason: String? = null) {
|
|
||||||
val error = when (code) {
|
|
||||||
429 -> "Rate limited. :("
|
|
||||||
else -> "Failed to connect"
|
|
||||||
}
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<ErrorResponse>(reason!!)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val message = parsed?.message ?: reason ?: error
|
|
||||||
val fullMessage = if (code == 500) message else "$code: $message"
|
|
||||||
|
|
||||||
toast(fullMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ErrorResponse(
|
|
||||||
@SerialName("message")
|
|
||||||
val message: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class NotificationResponse(
|
|
||||||
@SerialName("notifications")
|
|
||||||
val notifications: List<Notification>
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Notification(
|
|
||||||
@SerialName("username")
|
|
||||||
val username: String,
|
|
||||||
@SerialName("media_id")
|
|
||||||
val mediaId: Int,
|
|
||||||
@SerialName("comment_id")
|
|
||||||
val commentId: Int,
|
|
||||||
@SerialName("type")
|
|
||||||
val type: Int? = null,
|
|
||||||
@SerialName("content")
|
|
||||||
val content: String? = null,
|
|
||||||
@SerialName("notification_id")
|
|
||||||
val notificationId: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class AuthResponse(
|
|
||||||
@SerialName("authToken")
|
|
||||||
val authToken: String,
|
|
||||||
@SerialName("user")
|
|
||||||
val user: User
|
|
||||||
) : java.io.Serializable {
|
|
||||||
companion object {
|
|
||||||
private const val serialVersionUID: Long = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserResponse(
|
|
||||||
@SerialName("user")
|
|
||||||
val user: User
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class User(
|
|
||||||
@SerialName("user_id")
|
|
||||||
val id: String,
|
|
||||||
@SerialName("username")
|
|
||||||
val username: String,
|
|
||||||
@SerialName("profile_picture_url")
|
|
||||||
val profilePictureUrl: String? = null,
|
|
||||||
@SerialName("is_banned")
|
|
||||||
@Serializable(with = NumericBooleanSerializer::class)
|
|
||||||
val isBanned: Boolean? = null,
|
|
||||||
@SerialName("is_mod")
|
|
||||||
@Serializable(with = NumericBooleanSerializer::class)
|
|
||||||
val isAdmin: Boolean? = null,
|
|
||||||
@SerialName("is_admin")
|
|
||||||
@Serializable(with = NumericBooleanSerializer::class)
|
|
||||||
val isMod: Boolean? = null,
|
|
||||||
@SerialName("total_votes")
|
|
||||||
val totalVotes: Int,
|
|
||||||
@SerialName("warnings")
|
|
||||||
val warnings: Int
|
|
||||||
) : java.io.Serializable {
|
|
||||||
companion object {
|
|
||||||
private const val serialVersionUID: Long = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CommentResponse(
|
|
||||||
@SerialName("comments")
|
|
||||||
val comments: List<Comment>,
|
|
||||||
@SerialName("totalPages")
|
|
||||||
val totalPages: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Comment(
|
|
||||||
@SerialName("comment_id")
|
|
||||||
val commentId: Int,
|
|
||||||
@SerialName("user_id")
|
|
||||||
val userId: String,
|
|
||||||
@SerialName("media_id")
|
|
||||||
val mediaId: Int,
|
|
||||||
@SerialName("parent_comment_id")
|
|
||||||
val parentCommentId: Int?,
|
|
||||||
@SerialName("content")
|
|
||||||
var content: String,
|
|
||||||
@SerialName("timestamp")
|
|
||||||
var timestamp: String,
|
|
||||||
@SerialName("deleted")
|
|
||||||
@Serializable(with = NumericBooleanSerializer::class)
|
|
||||||
val deleted: Boolean?,
|
|
||||||
@SerialName("tag")
|
|
||||||
val tag: Int?,
|
|
||||||
@SerialName("upvotes")
|
|
||||||
var upvotes: Int,
|
|
||||||
@SerialName("downvotes")
|
|
||||||
var downvotes: Int,
|
|
||||||
@SerialName("user_vote_type")
|
|
||||||
var userVoteType: Int?,
|
|
||||||
@SerialName("username")
|
|
||||||
val username: String,
|
|
||||||
@SerialName("profile_picture_url")
|
|
||||||
val profilePictureUrl: String?,
|
|
||||||
@SerialName("is_mod")
|
|
||||||
@Serializable(with = NumericBooleanSerializer::class)
|
|
||||||
val isMod: Boolean? = null,
|
|
||||||
@SerialName("is_admin")
|
|
||||||
@Serializable(with = NumericBooleanSerializer::class)
|
|
||||||
val isAdmin: Boolean? = null,
|
|
||||||
@SerialName("reply_count")
|
|
||||||
val replyCount: Int? = null,
|
|
||||||
@SerialName("total_votes")
|
|
||||||
val totalVotes: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReturnedComment(
|
|
||||||
@SerialName("id")
|
|
||||||
var id: Int,
|
|
||||||
@SerialName("comment_id")
|
|
||||||
var commentId: Int?,
|
|
||||||
@SerialName("user_id")
|
|
||||||
val userId: String,
|
|
||||||
@SerialName("media_id")
|
|
||||||
val mediaId: Int,
|
|
||||||
@SerialName("parent_comment_id")
|
|
||||||
val parentCommentId: Int? = null,
|
|
||||||
@SerialName("content")
|
|
||||||
val content: String,
|
|
||||||
@SerialName("timestamp")
|
|
||||||
val timestamp: String,
|
|
||||||
@SerialName("deleted")
|
|
||||||
@Serializable(with = NumericBooleanSerializer::class)
|
|
||||||
val deleted: Boolean?,
|
|
||||||
@SerialName("tag")
|
|
||||||
val tag: Int? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
object NumericBooleanSerializer : KSerializer<Boolean> {
|
|
||||||
override val descriptor: SerialDescriptor =
|
|
||||||
PrimitiveSerialDescriptor("NumericBoolean", PrimitiveKind.INT)
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: Boolean) {
|
|
||||||
encoder.encodeInt(if (value) 1 else 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): Boolean {
|
|
||||||
return decoder.decodeInt() != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
package ani.dantotsu.connections.crashlytics
|
package ani.dantotsu.connections.crashlytics
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
|
|
||||||
class CrashlyticsStub : CrashlyticsInterface {
|
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)
|
//no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun log(message: String) {
|
override fun log(message: String) {
|
||||||
Logger.log(message)
|
//no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setUserId(id: String) {
|
override fun setUserId(id: String) {
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ object Discord {
|
|||||||
var avatar: String? = null
|
var avatar: String? = null
|
||||||
|
|
||||||
|
|
||||||
fun getSavedToken(): Boolean {
|
fun getSavedToken(context: Context): 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(token: String) {
|
fun saveToken(context: Context, token: String) {
|
||||||
PrefManager.setVal(PrefName.DiscordToken, token)
|
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,19 @@ 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:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
|
||||||
const val small_Image_AniList: String =
|
/*fun defaultRPC(): RPC? {
|
||||||
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
|
return token?.let {
|
||||||
|
RPC(it, Dispatchers.IO).apply {
|
||||||
|
applicationId = application_Id
|
||||||
|
smallImage = RPC.Link(
|
||||||
|
"Dantotsu",
|
||||||
|
small_Image
|
||||||
|
)
|
||||||
|
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,17 @@ 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 android.util.Log
|
||||||
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
|
||||||
@@ -21,7 +26,6 @@ import ani.dantotsu.connections.discord.serializers.User
|
|||||||
import ani.dantotsu.isOnline
|
import ani.dantotsu.isOnline
|
||||||
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.util.Logger
|
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
@@ -33,6 +37,7 @@ 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
|
||||||
@@ -44,7 +49,6 @@ 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
|
||||||
@@ -63,7 +67,7 @@ class DiscordService : Service() {
|
|||||||
PowerManager.PARTIAL_WAKE_LOCK,
|
PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
"discordRPC:backgroundPresence"
|
"discordRPC:backgroundPresence"
|
||||||
)
|
)
|
||||||
wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
|
wakeLock.acquire()
|
||||||
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(
|
||||||
@@ -158,8 +162,8 @@ class DiscordService : Service() {
|
|||||||
|
|
||||||
inner class DiscordWebSocketListener : WebSocketListener() {
|
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||||
|
|
||||||
private var retryAttempts = 0
|
var retryAttempts = 0
|
||||||
private val maxRetryAttempts = 10
|
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
|
||||||
@@ -228,7 +232,7 @@ class DiscordService : Service() {
|
|||||||
resume()
|
resume()
|
||||||
resume = false
|
resume = false
|
||||||
} else {
|
} else {
|
||||||
identify(webSocket)
|
identify(webSocket, baseContext)
|
||||||
log("WebSocket: Identified")
|
log("WebSocket: Identified")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,13 +245,13 @@ class DiscordService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun identify(webSocket: WebSocket) {
|
fun identify(webSocket: WebSocket, context: Context) {
|
||||||
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())
|
d.addProperty("token", getToken(context))
|
||||||
d.addProperty("intents", 0)
|
d.addProperty("intents", 0)
|
||||||
d.add("properties", properties)
|
d.add("properties", properties)
|
||||||
val payload = JsonObject()
|
val payload = JsonObject()
|
||||||
@@ -266,11 +270,11 @@ 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("Timeout setting presence", "Max Retry Attempts")
|
errorNotification("Could not set the presence", "Max Retry Attempts")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.message?.let { Logger.log("onFailure() $it") }
|
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
|
||||||
log("WebSocket: Error, onFailure() reason: ${t.message}")
|
log("WebSocket: Error, onFailure() reason: ${t.message}")
|
||||||
client = OkHttpClient()
|
client = OkHttpClient()
|
||||||
client.newWebSocket(
|
client.newWebSocket(
|
||||||
@@ -285,7 +289,7 @@ class DiscordService : Service() {
|
|||||||
|
|
||||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
super.onClosing(webSocket, code, reason)
|
super.onClosing(webSocket, code, reason)
|
||||||
Logger.log("onClosing() $code $reason")
|
Log.d("WebSocket", "onClosing() $code $reason")
|
||||||
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||||
heartbeatThread.interrupt()
|
heartbeatThread.interrupt()
|
||||||
}
|
}
|
||||||
@@ -293,7 +297,7 @@ class DiscordService : Service() {
|
|||||||
|
|
||||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
super.onClosed(webSocket, code, reason)
|
super.onClosed(webSocket, code, reason)
|
||||||
Logger.log("onClosed() $code $reason")
|
Log.d("WebSocket", "onClosed() $code $reason")
|
||||||
if (code >= 4000) {
|
if (code >= 4000) {
|
||||||
log("WebSocket: Error, code: $code reason: $reason")
|
log("WebSocket: Error, code: $code reason: $reason")
|
||||||
client = OkHttpClient()
|
client = OkHttpClient()
|
||||||
@@ -307,7 +311,7 @@ class DiscordService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToken(): String {
|
fun getToken(context: Context): 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")
|
||||||
@@ -345,13 +349,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
|
||||||
@@ -371,22 +375,65 @@ 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) {
|
||||||
if (shouldLog) {
|
Log.d("WebSocket_Discord", string)
|
||||||
Logger.log(string)
|
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLogToFile() {
|
||||||
|
val fileName = "log_${System.currentTimeMillis()}.txt"
|
||||||
|
|
||||||
|
// ContentValues to store file metadata
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserting the file in the MediaStore
|
||||||
|
val resolver = baseContext.contentResolver
|
||||||
|
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||||
|
} else {
|
||||||
|
val directory =
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
val file = File(directory, fileName)
|
||||||
|
|
||||||
|
// Make sure the Downloads directory exists
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use FileProvider to get the URI for the file
|
||||||
|
val authority =
|
||||||
|
"${baseContext.packageName}.provider" // Adjust with your app's package name
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the file
|
||||||
|
uri?.let {
|
||||||
|
resolver.openOutputStream(it).use { outputStream ->
|
||||||
|
OutputStreamWriter(outputStream).use { writer ->
|
||||||
|
writer.write(log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
log("Error saving log file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resume() {
|
fun resume() {
|
||||||
log("Sending Resume payload")
|
log("Sending Resume payload")
|
||||||
val d = JsonObject()
|
val d = JsonObject()
|
||||||
d.addProperty("token", getToken())
|
d.addProperty("token", getToken(baseContext))
|
||||||
d.addProperty("session_id", sessionId)
|
d.addProperty("session_id", sessionId)
|
||||||
d.addProperty("seq", sequence)
|
d.addProperty("seq", sequence)
|
||||||
val json = JsonObject()
|
val json = JsonObject()
|
||||||
@@ -402,7 +449,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 (ignored: InterruptedException) {
|
} catch (e: 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(token)
|
saveToken(this, token)
|
||||||
startMainActivity(this@Login)
|
startMainActivity(this@Login)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package ani.dantotsu.connections.discord
|
|||||||
|
|
||||||
import ani.dantotsu.connections.discord.serializers.Activity
|
import ani.dantotsu.connections.discord.serializers.Activity
|
||||||
import ani.dantotsu.connections.discord.serializers.Presence
|
import ani.dantotsu.connections.discord.serializers.Presence
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -71,8 +69,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 = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(),
|
smallImage = data.smallImage?.url?.discordUrl(),
|
||||||
smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu",
|
smallText = data.smallImage?.label
|
||||||
),
|
),
|
||||||
buttons = data.buttons.map { it.label },
|
buttons = data.buttons.map { it.label },
|
||||||
metadata = Activity.Metadata(
|
metadata = Activity.Metadata(
|
||||||
@@ -83,7 +81,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
|||||||
),
|
),
|
||||||
afk = true,
|
afk = true,
|
||||||
since = data.startTimestamp,
|
since = data.startTimestamp,
|
||||||
status = PrefManager.getVal(PrefName.DiscordStatus)
|
status = data.status
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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,6 +5,7 @@ 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
|
||||||
@@ -63,7 +64,7 @@ object MAL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun getSavedToken(): Boolean {
|
suspend fun getSavedToken(context: FragmentActivity): 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)
|
||||||
@@ -76,7 +77,7 @@ object MAL {
|
|||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSavedToken() {
|
fun removeSavedToken(context: Context) {
|
||||||
token = null
|
token = null
|
||||||
username = null
|
username = null
|
||||||
userid = null
|
userid = null
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
package ani.dantotsu.download
|
package ani.dantotsu.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.os.Environment
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import android.widget.Toast
|
||||||
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 kotlinx.coroutines.CoroutineScope
|
import java.io.File
|
||||||
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) {
|
||||||
@@ -27,11 +15,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 == MediaType.MANGA }
|
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
||||||
val animeDownloadedTypes: List<DownloadedType>
|
val animeDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == MediaType.ANIME }
|
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
||||||
val novelDownloadedTypes: List<DownloadedType>
|
val novelDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == MediaType.NOVEL }
|
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
||||||
|
|
||||||
private fun saveDownloads() {
|
private fun saveDownloads() {
|
||||||
val jsonString = gson.toJson(downloadsList)
|
val jsonString = gson.toJson(downloadsList)
|
||||||
@@ -53,70 +41,84 @@ class DownloadsManager(private val context: Context) {
|
|||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeDownload(
|
fun removeDownload(downloadedType: DownloadedType) {
|
||||||
downloadedType: DownloadedType,
|
|
||||||
toast: Boolean = true,
|
|
||||||
onFinished: () -> Unit
|
|
||||||
) {
|
|
||||||
downloadsList.remove(downloadedType)
|
downloadsList.remove(downloadedType)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
removeDirectory(downloadedType)
|
||||||
removeDirectory(downloadedType, toast)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
onFinished()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMedia(title: String, type: MediaType) {
|
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||||
val baseDirectory = getBaseDirectory(context, type)
|
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||||
val directory = baseDirectory?.findFolder(title)
|
"Manga"
|
||||||
if (directory?.exists() == true) {
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
val deleted = directory.deleteRecursively(context, false)
|
"Anime"
|
||||||
|
} else {
|
||||||
|
"Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$subDirectory/$title"
|
||||||
|
)
|
||||||
|
if (directory.exists()) {
|
||||||
|
val deleted = directory.deleteRecursively()
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
snackString("Successfully deleted")
|
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
snackString("Failed to delete directory")
|
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
snackString("Directory does not exist")
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
cleanDownloads()
|
cleanDownloads()
|
||||||
}
|
}
|
||||||
when (type) {
|
when (type) {
|
||||||
MediaType.MANGA -> {
|
DownloadedType.Type.MANGA -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaType.ANIME -> {
|
DownloadedType.Type.ANIME -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaType.NOVEL -> {
|
DownloadedType.Type.NOVEL -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
|
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanDownloads() {
|
private fun cleanDownloads() {
|
||||||
cleanDownload(MediaType.MANGA)
|
cleanDownload(DownloadedType.Type.MANGA)
|
||||||
cleanDownload(MediaType.ANIME)
|
cleanDownload(DownloadedType.Type.ANIME)
|
||||||
cleanDownload(MediaType.NOVEL)
|
cleanDownload(DownloadedType.Type.NOVEL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanDownload(type: MediaType) {
|
private fun cleanDownload(type: DownloadedType.Type) {
|
||||||
// remove all folders that are not in the downloads list
|
// remove all folders that are not in the downloads list
|
||||||
val directory = getBaseDirectory(context, type)
|
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||||
val downloadsSubLists = when (type) {
|
"Manga"
|
||||||
MediaType.MANGA -> mangaDownloadedTypes
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
MediaType.ANIME -> animeDownloadedTypes
|
"Anime"
|
||||||
else -> novelDownloadedTypes
|
} else {
|
||||||
|
"Novel"
|
||||||
}
|
}
|
||||||
if (directory?.exists() == true && directory.isDirectory) {
|
val directory = File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$subDirectory"
|
||||||
|
)
|
||||||
|
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
||||||
|
mangaDownloadedTypes
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
animeDownloadedTypes
|
||||||
|
} else {
|
||||||
|
novelDownloadedTypes
|
||||||
|
}
|
||||||
|
if (directory.exists()) {
|
||||||
val files = directory.listFiles()
|
val files = directory.listFiles()
|
||||||
for (file in files) {
|
if (files != null) {
|
||||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
for (file in files) {
|
||||||
file.deleteRecursively(context, false)
|
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||||
|
val deleted = file.deleteRecursively()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,92 +126,34 @@ 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 = directory?.findFolder(download.title)
|
val downloadDir = File(directory, download.title)
|
||||||
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
|
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveDownloadsDir(
|
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||||
context: Context,
|
{
|
||||||
oldUri: Uri,
|
val jsonString = gson.toJson(downloadsList)
|
||||||
newUri: Uri,
|
val file = File(
|
||||||
finished: (Boolean, String) -> Unit
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
) {
|
"Dantotsu/downloads.json"
|
||||||
try {
|
)
|
||||||
if (oldUri == newUri) {
|
if (file.parentFile?.exists() == false) {
|
||||||
finished(false, "Source and destination are the same")
|
file.parentFile?.mkdirs()
|
||||||
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: MediaType? = null): Boolean {
|
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
||||||
return if (type == null) {
|
return if (type == null) {
|
||||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||||
} else {
|
} else {
|
||||||
@@ -217,35 +161,87 @@ class DownloadsManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
|
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||||
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
val directory =
|
File(
|
||||||
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
downloadsList.remove(downloadedType)
|
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the directory exists and delete it recursively
|
// Check if the directory exists and delete it recursively
|
||||||
if (directory?.exists() == true) {
|
if (directory.exists()) {
|
||||||
val deleted = directory.deleteRecursively(context, false)
|
val deleted = directory.deleteRecursively()
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
if (toast) snackString("Successfully deleted")
|
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
snackString("Failed to delete directory")
|
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
snackString("Directory does not exist")
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun purgeDownloads(type: MediaType) {
|
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||||
val directory = getBaseDirectory(context, type)
|
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||||
if (directory?.exists() == true) {
|
File(
|
||||||
val deleted = directory.deleteRecursively(context, false)
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
if (deleted) {
|
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
snackString("Successfully deleted")
|
)
|
||||||
|
} 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 {
|
} else {
|
||||||
snackString("Failed to delete directory")
|
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
snackString("Directory does not exist")
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun purgeDownloads(type: DownloadedType.Type) {
|
||||||
|
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||||
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||||
|
} else {
|
||||||
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||||
|
}
|
||||||
|
if (directory.exists()) {
|
||||||
|
val deleted = directory.deleteRecursively()
|
||||||
|
if (deleted) {
|
||||||
|
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadsList.removeAll { it.type == type }
|
downloadsList.removeAll { it.type == type }
|
||||||
@@ -253,126 +249,62 @@ class DownloadsManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BASE_LOCATION = "Dantotsu"
|
const val novelLocation = "Dantotsu/Novel"
|
||||||
private const val MANGA_SUB_LOCATION = "Manga"
|
const val mangaLocation = "Dantotsu/Manga"
|
||||||
private const val ANIME_SUB_LOCATION = "Anime"
|
const val animeLocation = "Dantotsu/Anime"
|
||||||
private const val NOVEL_SUB_LOCATION = "Novel"
|
|
||||||
|
|
||||||
|
fun getDirectory(
|
||||||
/**
|
|
||||||
* Get and create a base directory for the given type
|
|
||||||
* @param context the context
|
|
||||||
* @param type the type of media
|
|
||||||
* @return the base directory
|
|
||||||
*/
|
|
||||||
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
|
|
||||||
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
|
|
||||||
if (baseDirectory == Uri.EMPTY) return null
|
|
||||||
var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
|
|
||||||
base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
|
|
||||||
return when (type) {
|
|
||||||
MediaType.MANGA -> {
|
|
||||||
base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaType.ANIME -> {
|
|
||||||
base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,
|
context: Context,
|
||||||
type: MediaType,
|
type: DownloadedType.Type,
|
||||||
overwrite: Boolean,
|
|
||||||
title: String,
|
title: String,
|
||||||
chapter: String? = null
|
chapter: String? = null
|
||||||
): DocumentFile? {
|
): File {
|
||||||
val baseDirectory = getBaseDirectory(context, type) ?: return null
|
return if (type == DownloadedType.Type.MANGA) {
|
||||||
return if (chapter != null) {
|
if (chapter != null) {
|
||||||
baseDirectory.findOrCreateFolder(title, false)
|
File(
|
||||||
?.findOrCreateFolder(chapter, overwrite)
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$mangaLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$mangaLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (type == DownloadedType.Type.ANIME) {
|
||||||
|
if (chapter != null) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$animeLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$animeLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
baseDirectory.findOrCreateFolder(title, overwrite)
|
if (chapter != null) {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$novelLocation/$title/$chapter"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"$novelLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDirSize(
|
|
||||||
context: Context,
|
|
||||||
type: MediaType,
|
|
||||||
title: String,
|
|
||||||
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 = "|\\?*<\":>+[]/'"
|
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||||
private fun String?.findValidName(): String {
|
enum class Type {
|
||||||
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
MANGA,
|
||||||
}
|
ANIME,
|
||||||
|
NOVEL
|
||||||
data class DownloadedType(
|
}
|
||||||
val pTitle: String, val pChapter: String, val type: MediaType
|
|
||||||
) : Serializable {
|
|
||||||
val title: String
|
|
||||||
get() = pTitle.findValidName()
|
|
||||||
val chapter: String
|
|
||||||
get() = pChapter.findValidName()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,35 +9,32 @@ 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.defaultHeaders
|
import ani.dantotsu.currActivity
|
||||||
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.getSubDirectory
|
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||||
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
import ani.dantotsu.download.video.Helper
|
||||||
|
import ani.dantotsu.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
|
||||||
@@ -48,7 +45,9 @@ 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
|
||||||
@@ -57,12 +56,13 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlinx.coroutines.withContext
|
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
|
||||||
|
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
|
||||||
@@ -73,7 +73,6 @@ 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,11 +81,6 @@ 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 {
|
||||||
@@ -94,7 +88,6 @@ 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(
|
||||||
@@ -163,14 +156,27 @@ class AnimeDownloaderService : Service() {
|
|||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun cancelDownload(taskName: String) {
|
fun cancelDownload(taskName: String) {
|
||||||
val sessionIds =
|
val url =
|
||||||
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
|
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
|
||||||
.map { it.sessionId }.toMutableList()
|
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
||||||
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
if (url.isEmpty()) {
|
||||||
sessionIds.forEach {
|
snackString("Failed to cancel download")
|
||||||
ffExtension!!.cancelDownload(it)
|
return
|
||||||
}
|
}
|
||||||
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()
|
||||||
@@ -203,6 +209,7 @@ 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(
|
||||||
@@ -213,63 +220,18 @@ class AnimeDownloaderService : Service() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
|
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
||||||
if (notifi) {
|
if (notifi) {
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
val outputDir = getSubDirectory(
|
currActivity()?.let {
|
||||||
this@AnimeDownloaderService,
|
Helper.downloadVideo(
|
||||||
MediaType.ANIME,
|
it,
|
||||||
false,
|
task.video,
|
||||||
task.title,
|
task.subtitle
|
||||||
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 {
|
||||||
@@ -279,124 +241,94 @@ class AnimeDownloaderService : Service() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.episode,
|
task.episode,
|
||||||
MediaType.ANIME,
|
DownloadedType.Type.ANIME,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val downloadStarted =
|
||||||
|
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
|
||||||
|
|
||||||
|
if (!downloadStarted) {
|
||||||
|
logger("Download failed to start")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download failed to start")
|
||||||
|
broadcastDownloadFailed(task.episode)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// periodically check if the download is complete
|
// periodically check if the download is complete
|
||||||
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
||||||
if (ffExtension.getState(ffTask) == "FAILED") {
|
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||||
Logger.log("Download failed")
|
if (download != null) {
|
||||||
builder.setContentText(
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
||||||
"${
|
logger("Download failed")
|
||||||
getTaskName(
|
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download failed")
|
||||||
|
logger("Download failed: ${download.failureReason}")
|
||||||
|
downloadsManager.removeDownload(
|
||||||
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.episode
|
task.episode,
|
||||||
|
DownloadedType.Type.ANIME,
|
||||||
)
|
)
|
||||||
} Download failed"
|
|
||||||
)
|
|
||||||
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}"
|
|
||||||
)
|
)
|
||||||
|
Injekt.get<CrashlyticsInterface>().logException(
|
||||||
|
Exception(
|
||||||
|
"Anime Download failed:" +
|
||||||
|
" ${download.failureReason}" +
|
||||||
|
" url: ${task.video.file.url}" +
|
||||||
|
" title: ${task.title}" +
|
||||||
|
" episode: ${task.episode}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
|
broadcastDownloadFailed(task.episode)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
|
||||||
|
logger("Download completed")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download completed")
|
||||||
|
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||||
|
task.getTaskName(),
|
||||||
|
task.video.file.url
|
||||||
|
).apply()
|
||||||
|
downloadsManager.addDownload(
|
||||||
|
DownloadedType(
|
||||||
|
task.title,
|
||||||
|
task.episode,
|
||||||
|
DownloadedType.Type.ANIME,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
|
broadcastDownloadFinished(task.episode)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
|
||||||
|
logger("Download stopped")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download stopped")
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download stopped")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
broadcastDownloadProgress(
|
||||||
|
task.episode,
|
||||||
|
download.percentDownloaded.toInt()
|
||||||
)
|
)
|
||||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
if (notifi) {
|
||||||
broadcastDownloadFailed(task.episode)
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
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
|
||||||
Logger.log("Exception while downloading file: ${e.message}")
|
logger("Exception while downloading file: ${e.message}")
|
||||||
snackString("Exception while downloading file: ${e.message}")
|
snackString("Exception while downloading file: ${e.message}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
@@ -405,24 +337,36 @@ class AnimeDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
suspend fun hasDownloadStarted(
|
||||||
val directory =
|
downloadManager: DownloadManager,
|
||||||
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
|
task: AnimeDownloadTask,
|
||||||
?: throw Exception("Directory not found")
|
timeout: Long
|
||||||
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
|
): Boolean {
|
||||||
val file = directory.createFile("application/json", "media.json")
|
val startTime = System.currentTimeMillis()
|
||||||
?: throw Exception("File not created")
|
while (System.currentTimeMillis() - startTime < timeout) {
|
||||||
val episodeDirectory =
|
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||||
getSubDirectory(
|
if (download != null) {
|
||||||
this@AnimeDownloaderService,
|
return true
|
||||||
MediaType.ANIME,
|
}
|
||||||
false,
|
// Delay between each poll
|
||||||
task.title,
|
kotlinx.coroutines.delay(500)
|
||||||
task.episode
|
}
|
||||||
)
|
return false
|
||||||
?: throw Exception("Directory not found")
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val directory = File(
|
||||||
|
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"${DownloadsManager.animeLocation}/${task.title}"
|
||||||
|
)
|
||||||
|
val episodeDirectory = File(directory, task.episode)
|
||||||
|
if (!directory.exists()) directory.mkdirs()
|
||||||
|
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
||||||
|
|
||||||
|
val file = File(directory, "media.json")
|
||||||
val gson = GsonBuilder()
|
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
|
||||||
@@ -456,25 +400,14 @@ class AnimeDownloaderService : Service() {
|
|||||||
|
|
||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
file.writeText(jsonString)
|
||||||
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")
|
||||||
@@ -485,16 +418,13 @@ class AnimeDownloaderService : Service() {
|
|||||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
}
|
}
|
||||||
|
|
||||||
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
|
val file = File(directory, name)
|
||||||
val file =
|
FileOutputStream(file).use { output ->
|
||||||
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.uri.toString()
|
return@withContext file.absolutePath
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -561,15 +491,14 @@ 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.replace("/", "")}/${episode.replace("/", "")}"
|
return "$title - $episode"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getTaskName(title: String, episode: String): String {
|
fun getTaskName(title: String, episode: String): String {
|
||||||
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
return "$title - $episode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,6 +512,7 @@ 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,6 +1,7 @@
|
|||||||
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
|
||||||
@@ -37,6 +38,7 @@ 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) {
|
||||||
@@ -49,27 +51,28 @@ 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 = context.getString(R.string.episodes)
|
episodes.text = " Episodes"
|
||||||
bannerView.setImageURI(item.banner ?: item.image)
|
bannerView.setImageURI(item.banner)
|
||||||
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 = context.getString(R.string.total_divider, item.totalEpisode)
|
totalepisodes.text = " | " + 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,6 +4,7 @@ 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
|
||||||
@@ -21,10 +22,8 @@ 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
|
||||||
@@ -33,19 +32,16 @@ import ani.dantotsu.currActivity
|
|||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
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.initActivity
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.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
|
||||||
@@ -57,13 +53,9 @@ 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 {
|
||||||
|
|
||||||
@@ -72,7 +64,6 @@ 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,
|
||||||
@@ -119,10 +110,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
|
||||||
@@ -143,7 +134,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)
|
||||||
@@ -163,11 +154,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"
|
||||||
@@ -175,22 +166,20 @@ 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.compareName(item.title) }
|
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
media?.let {
|
media?.let {
|
||||||
lifecycleScope.launch {
|
val mediaModel = getMedia(it)
|
||||||
val mediaModel = getMedia(it)
|
if (mediaModel == null) {
|
||||||
if (mediaModel == null) {
|
snackString("Error loading media.json")
|
||||||
snackString("Error loading media.json")
|
return@let
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -198,7 +187,8 @@ 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: MediaType = MediaType.ANIME
|
val type: DownloadedType.Type =
|
||||||
|
DownloadedType.Type.ANIME
|
||||||
|
|
||||||
// Alert dialog to confirm deletion
|
// Alert dialog to confirm deletion
|
||||||
val builder =
|
val builder =
|
||||||
@@ -213,7 +203,13 @@ 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
|
||||||
@@ -241,6 +237,7 @@ 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(
|
||||||
@@ -253,7 +250,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.isVisible = visibility
|
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
initActivity(requireActivity())
|
initActivity(requireActivity())
|
||||||
@@ -263,6 +260,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
getDownloads()
|
getDownloads()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -282,39 +280,29 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
|
|
||||||
private fun getDownloads() {
|
private fun getDownloads() {
|
||||||
downloads = listOf()
|
downloads = listOf()
|
||||||
if (downloadsJob.isActive) {
|
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||||
downloadsJob.cancel()
|
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||||
}
|
for (title in animeTitles) {
|
||||||
downloadsJob = Job()
|
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
val download = tDownloads.first()
|
||||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
newAnimeDownloads += offlineAnimeModel
|
||||||
for (title in animeTitles) {
|
|
||||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
|
||||||
val download = tDownloads.first()
|
|
||||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
|
||||||
newAnimeDownloads += offlineAnimeModel
|
|
||||||
}
|
|
||||||
downloads = newAnimeDownloads
|
|
||||||
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? {
|
||||||
* Load media.json file from the directory and convert it to Media class
|
val type = when (downloadedType.type) {
|
||||||
* @param downloadedType DownloadedType object
|
DownloadedType.Type.MANGA -> "Manga"
|
||||||
* @return Media object
|
DownloadedType.Type.ANIME -> "Anime"
|
||||||
*/
|
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.title
|
|
||||||
)
|
|
||||||
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
|
||||||
@@ -326,42 +314,37 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||||
})
|
})
|
||||||
.create()
|
.create()
|
||||||
val media = directory?.findFile("media.json")
|
val media = File(directory, "media.json")
|
||||||
?: return null
|
val mediaJson = media.readText()
|
||||||
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("Error loading media.json: ${e.message}")
|
||||||
Logger.log(e)
|
logger(e.printStackTrace())
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||||
* Load OfflineAnimeModel from the directory
|
val type = when (downloadedType.type) {
|
||||||
* @param downloadedType DownloadedType object
|
DownloadedType.Type.MANGA -> "Manga"
|
||||||
* @return OfflineAnimeModel object
|
DownloadedType.Type.ANIME -> "Anime"
|
||||||
*/
|
else -> "Novel"
|
||||||
private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
}
|
||||||
val type = downloadedType.type.asText()
|
val directory = File(
|
||||||
|
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.title
|
|
||||||
)
|
|
||||||
val mediaModel = getMedia(downloadedType)!!
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
val cover = directory?.findFile("cover.jpg")
|
val cover = File(directory, "cover.jpg")
|
||||||
val coverUri: Uri? = if (cover?.exists() == true) {
|
val coverUri: Uri? = if (cover.exists()) {
|
||||||
cover.uri
|
Uri.fromFile(cover)
|
||||||
} else null
|
} else null
|
||||||
val banner = directory?.findFile("banner.jpg")
|
val banner = File(directory, "banner.jpg")
|
||||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
val bannerUri: Uri? = if (banner.exists()) {
|
||||||
banner.uri
|
Uri.fromFile(banner)
|
||||||
} else null
|
} else null
|
||||||
val title = mediaModel.mainName()
|
val title = mediaModel.mainName()
|
||||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||||
@@ -391,8 +374,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||||||
bannerUri
|
bannerUri
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error loading media.json: ${e.message}")
|
logger("Error loading media.json: ${e.message}")
|
||||||
Logger.log(e)
|
logger(e.printStackTrace())
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
return OfflineAnimeModel(
|
return OfflineAnimeModel(
|
||||||
"unknown",
|
"unknown",
|
||||||
|
|||||||
@@ -10,20 +10,19 @@ 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.download.DownloadsManager.Companion.getSubDirectory
|
import ani.dantotsu.logger
|
||||||
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
|
||||||
@@ -31,10 +30,6 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
|
|||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.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
|
||||||
@@ -42,8 +37,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
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
|
||||||
@@ -52,9 +47,10 @@ 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
|
||||||
@@ -191,20 +187,13 @@ 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()) {
|
||||||
@@ -220,7 +209,8 @@ 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++
|
||||||
}
|
}
|
||||||
@@ -254,14 +244,14 @@ class MangaDownloaderService : Service() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.chapter,
|
task.chapter,
|
||||||
MediaType.MANGA
|
DownloadedType.Type.MANGA
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
broadcastDownloadFinished(task.chapter)
|
broadcastDownloadFinished(task.chapter)
|
||||||
snackString("${task.title} - ${task.chapter} Download finished")
|
snackString("${task.title} - ${task.chapter} Download finished")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Exception while downloading file: ${e.message}")
|
logger("Exception while downloading file: ${e.message}")
|
||||||
snackString("Exception while downloading file: ${e.message}")
|
snackString("Exception while downloading file: ${e.message}")
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
broadcastDownloadFailed(task.chapter)
|
broadcastDownloadFailed(task.chapter)
|
||||||
@@ -272,18 +262,24 @@ 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 = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
|
val directory = File(
|
||||||
?: throw Exception("Directory not found")
|
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
directory.findFile(fileName)?.forceDelete(this)
|
"Dantotsu/Manga/$title/$chapter"
|
||||||
// Create a file reference within that directory for the image
|
)
|
||||||
val file =
|
|
||||||
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a file reference within that directory for your image
|
||||||
|
val file = File(directory, fileName)
|
||||||
|
|
||||||
// Use a FileOutputStream to write the bitmap to the file
|
// Use a FileOutputStream to write the bitmap to the file
|
||||||
file.openOutputStream(this, false).use { outputStream ->
|
FileOutputStream(file).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}")
|
||||||
@@ -291,15 +287,15 @@ class MangaDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
private fun saveMediaInfo(task: DownloadTask) {
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
launchIO {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
val directory =
|
val directory = File(
|
||||||
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
|
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
?: throw Exception("Directory not found")
|
"Dantotsu/Manga/${task.title}"
|
||||||
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
|
)
|
||||||
val file = directory.createFile("application/json", "media.json")
|
if (!directory.exists()) directory.mkdirs()
|
||||||
?: 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
|
||||||
@@ -314,10 +310,7 @@ class MangaDownloaderService : Service() {
|
|||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
file.writeText(jsonString)
|
||||||
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(
|
||||||
@@ -332,7 +325,7 @@ class MangaDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -342,16 +335,14 @@ 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 =
|
val file = File(directory, name)
|
||||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
FileOutputStream(file).use { output ->
|
||||||
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.uri.toString()
|
return@withContext file.absolutePath
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -36,6 +37,7 @@ 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) {
|
||||||
@@ -48,6 +50,7 @@ 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)
|
||||||
@@ -57,14 +60,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 = context.getString(R.string.chapters)
|
chapters.text = " Chapters"
|
||||||
bannerView.setImageURI(item.banner ?: item.image)
|
bannerView.setImageURI(item.banner)
|
||||||
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 = context.getString(R.string.total_divider, item.totalChapter)
|
totalChapter.text = " | " + item.totalChapter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind item data to the views
|
// Bind item data to the views
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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
|
||||||
@@ -19,10 +20,8 @@ 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
|
||||||
@@ -30,20 +29,16 @@ import ani.dantotsu.currActivity
|
|||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
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.initActivity
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.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
|
||||||
@@ -51,13 +46,9 @@ 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 {
|
||||||
|
|
||||||
@@ -66,7 +57,6 @@ 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,
|
||||||
@@ -156,11 +146,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 =
|
||||||
@@ -169,18 +159,17 @@ 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.compareName(item.title) }
|
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
|
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == 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")
|
||||||
}
|
}
|
||||||
@@ -189,11 +178,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: MediaType =
|
val type: DownloadedType.Type =
|
||||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||||
MediaType.MANGA
|
DownloadedType.Type.MANGA
|
||||||
} else {
|
} else {
|
||||||
MediaType.NOVEL
|
DownloadedType.Type.NOVEL
|
||||||
}
|
}
|
||||||
// Alert dialog to confirm deletion
|
// Alert dialog to confirm deletion
|
||||||
val builder =
|
val builder =
|
||||||
@@ -203,6 +192,9 @@ 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
|
||||||
@@ -231,6 +223,7 @@ 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(
|
||||||
@@ -241,7 +234,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.isVisible = visibility
|
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||||
scrollTop.translationY =
|
scrollTop.translationY =
|
||||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||||
}
|
}
|
||||||
@@ -253,6 +246,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
getDownloads()
|
getDownloads()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -272,87 +266,75 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
|
|
||||||
private fun getDownloads() {
|
private fun getDownloads() {
|
||||||
downloads = listOf()
|
downloads = listOf()
|
||||||
if (downloadsJob.isActive) {
|
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||||
downloadsJob.cancel()
|
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
|
for (title in mangaTitles) {
|
||||||
|
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||||
|
val download = tDownloads.first()
|
||||||
|
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||||
|
newMangaDownloads += offlineMangaModel
|
||||||
}
|
}
|
||||||
downloads = listOf()
|
downloads = newMangaDownloads
|
||||||
downloadsJob = Job()
|
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
for (title in novelTitles) {
|
||||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||||
for (title in mangaTitles) {
|
val download = tDownloads.first()
|
||||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||||
val download = tDownloads.first()
|
newNovelDownloads += offlineMangaModel
|
||||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
|
||||||
newMangaDownloads += offlineMangaModel
|
|
||||||
}
|
|
||||||
downloads = newMangaDownloads
|
|
||||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
|
||||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
|
||||||
for (title in novelTitles) {
|
|
||||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
|
||||||
val download = tDownloads.first()
|
|
||||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
|
||||||
newNovelDownloads += offlineMangaModel
|
|
||||||
}
|
|
||||||
downloads += newNovelDownloads
|
|
||||||
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? {
|
||||||
* Load media.json file from the directory and convert it to Media class
|
val type = when (downloadedType.type) {
|
||||||
* @param downloadedType DownloadedType object
|
DownloadedType.Type.MANGA -> "Manga"
|
||||||
* @return Media object
|
DownloadedType.Type.ANIME -> "Anime"
|
||||||
*/
|
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.title
|
|
||||||
)
|
|
||||||
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 = directory?.findFile("media.json")
|
val media = File(directory, "media.json")
|
||||||
?: return null
|
val mediaJson = media.readText()
|
||||||
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("Error loading media.json: ${e.message}")
|
||||||
Logger.log(e)
|
logger(e.printStackTrace())
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||||
val type = downloadedType.type.asText()
|
val type = when (downloadedType.type) {
|
||||||
|
DownloadedType.Type.MANGA -> "Manga"
|
||||||
|
DownloadedType.Type.ANIME -> "Anime"
|
||||||
|
else -> "Novel"
|
||||||
|
}
|
||||||
|
val directory = File(
|
||||||
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
)
|
||||||
//load media.json and convert to media class with gson
|
//load media.json and convert to media class with gson
|
||||||
try {
|
try {
|
||||||
val directory = getSubDirectory(
|
|
||||||
context ?: currContext()!!, downloadedType.type,
|
|
||||||
false, downloadedType.title
|
|
||||||
)
|
|
||||||
val mediaModel = getMedia(downloadedType)!!
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
val cover = directory?.findFile("cover.jpg")
|
val cover = File(directory, "cover.jpg")
|
||||||
val coverUri: Uri? = if (cover?.exists() == true) {
|
val coverUri: Uri? = if (cover.exists()) {
|
||||||
cover.uri
|
Uri.fromFile(cover)
|
||||||
} else null
|
} else null
|
||||||
val banner = directory?.findFile("banner.jpg")
|
val banner = File(directory, "banner.jpg")
|
||||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
val bannerUri: Uri? = if (banner.exists()) {
|
||||||
banner.uri
|
Uri.fromFile(banner)
|
||||||
} else null
|
} else null
|
||||||
val title = mediaModel.mainName()
|
val title = mediaModel.mainName()
|
||||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||||
@@ -360,14 +342,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
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,
|
||||||
@@ -376,8 +358,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||||||
bannerUri
|
bannerUri
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error loading media.json: ${e.message}")
|
logger("Error loading media.json: ${e.message}")
|
||||||
Logger.log(e)
|
logger(e.printStackTrace())
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
return OfflineMangaModel(
|
return OfflineMangaModel(
|
||||||
"unknown",
|
"unknown",
|
||||||
|
|||||||
@@ -9,25 +9,21 @@ 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.download.DownloadsManager.Companion.getSubDirectory
|
import ani.dantotsu.logger
|
||||||
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
|
||||||
@@ -35,8 +31,8 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
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
|
||||||
@@ -46,9 +42,10 @@ import kotlinx.coroutines.withContext
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
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
|
||||||
@@ -65,7 +62,7 @@ class NovelDownloaderService : Service() {
|
|||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private var isCurrentlyProcessing = false
|
private var isCurrentlyProcessing = false
|
||||||
|
|
||||||
private val networkHelper = Injekt.get<NetworkHelper>()
|
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.
|
||||||
@@ -189,15 +186,15 @@ class NovelDownloaderService : Service() {
|
|||||||
val contentType = response.header("Content-Type")
|
val contentType = response.header("Content-Type")
|
||||||
val contentDisposition = response.header("Content-Disposition")
|
val contentDisposition = response.header("Content-Disposition")
|
||||||
|
|
||||||
Logger.log("Content-Type: $contentType")
|
logger("Content-Type: $contentType")
|
||||||
Logger.log("Content-Disposition: $contentDisposition")
|
logger("Content-Disposition: $contentDisposition")
|
||||||
|
|
||||||
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
|
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
|
||||||
contentType == "application/epub+zip" ||
|
contentType == "application/epub+zip" ||
|
||||||
(contentDisposition?.contains(".epub") == true)
|
(contentDisposition?.contains(".epub") == true)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error checking file type: ${e.message}")
|
logger("Error checking file type: ${e.message}")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,12 +225,12 @@ class NovelDownloaderService : Service() {
|
|||||||
|
|
||||||
if (!isEpubFile(task.downloadLink)) {
|
if (!isEpubFile(task.downloadLink)) {
|
||||||
if (isAlreadyDownloaded(task.originalLink)) {
|
if (isAlreadyDownloaded(task.originalLink)) {
|
||||||
Logger.log("Already downloaded")
|
logger("Already downloaded")
|
||||||
broadcastDownloadFinished(task.originalLink)
|
broadcastDownloadFinished(task.originalLink)
|
||||||
snackString("Already downloaded")
|
snackString("Already downloaded")
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
Logger.log("Download link is not an .epub file")
|
logger("Download link is not an .epub file")
|
||||||
broadcastDownloadFailed(task.originalLink)
|
broadcastDownloadFailed(task.originalLink)
|
||||||
snackString("Download link is not an .epub file")
|
snackString("Download link is not an .epub file")
|
||||||
return@withContext
|
return@withContext
|
||||||
@@ -248,30 +245,27 @@ 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) {
|
if (!response.isSuccessful || response.body == null) {
|
||||||
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 = directory.createFile("application/epub+zip", "0.epub")
|
val file = File(
|
||||||
?: throw Exception("File not created")
|
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create directories if they don't exist
|
||||||
|
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
|
||||||
|
|
||||||
|
// Overwrite existing file
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
|
||||||
//download cover
|
//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 = outputStream.sink().buffer()
|
val sink = file.sink().buffer()
|
||||||
val responseBody = response.body
|
val responseBody = response.body
|
||||||
val totalBytes = responseBody.contentLength()
|
val totalBytes = responseBody.contentLength()
|
||||||
var downloadedBytes = 0L
|
var downloadedBytes = 0L
|
||||||
@@ -307,7 +301,7 @@ class NovelDownloaderService : Service() {
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val progress =
|
val progress =
|
||||||
(downloadedBytes * 100 / totalBytes).toInt()
|
(downloadedBytes * 100 / totalBytes).toInt()
|
||||||
Logger.log("Download progress: $progress")
|
logger("Download progress: $progress")
|
||||||
broadcastDownloadProgress(task.originalLink, progress)
|
broadcastDownloadProgress(task.originalLink, progress)
|
||||||
}
|
}
|
||||||
lastBroadcastUpdate = downloadedBytes
|
lastBroadcastUpdate = downloadedBytes
|
||||||
@@ -322,7 +316,7 @@ class NovelDownloaderService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Exception while downloading .epub inside request: ${e.message}")
|
logger("Exception while downloading .epub inside request: ${e.message}")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,33 +333,29 @@ class NovelDownloaderService : Service() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.chapter,
|
task.chapter,
|
||||||
MediaType.NOVEL
|
DownloadedType.Type.NOVEL
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
broadcastDownloadFinished(task.originalLink)
|
broadcastDownloadFinished(task.originalLink)
|
||||||
snackString("${task.title} - ${task.chapter} Download finished")
|
snackString("${task.title} - ${task.chapter} Download finished")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Exception while downloading .epub: ${e.message}")
|
logger("Exception while downloading .epub: ${e.message}")
|
||||||
snackString("Exception while downloading .epub: ${e.message}")
|
snackString("Exception while downloading .epub: ${e.message}")
|
||||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||||
broadcastDownloadFailed(task.originalLink)
|
broadcastDownloadFailed(task.originalLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
private fun saveMediaInfo(task: DownloadTask) {
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
launchIO {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
val directory =
|
val directory = File(
|
||||||
getSubDirectory(
|
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
this@NovelDownloaderService,
|
"Dantotsu/Novel/${task.title}"
|
||||||
MediaType.NOVEL,
|
)
|
||||||
false,
|
if (!directory.exists()) directory.mkdirs()
|
||||||
task.title
|
|
||||||
) ?: throw Exception("Directory not found")
|
val file = File(directory, "media.json")
|
||||||
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
|
||||||
@@ -379,47 +369,33 @@ class NovelDownloaderService : Service() {
|
|||||||
|
|
||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
file.writeText(jsonString)
|
||||||
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: DocumentFile, name: String): String? =
|
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||||
withContext(
|
withContext(
|
||||||
Dispatchers.IO
|
Dispatchers.IO
|
||||||
) {
|
) {
|
||||||
var connection: HttpURLConnection? = null
|
var connection: HttpURLConnection? = null
|
||||||
Logger.log("Downloading url $url")
|
println("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 =
|
val file = File(directory, name)
|
||||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
FileOutputStream(file).use { output ->
|
||||||
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.uri.toString()
|
return@withContext file.absolutePath
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -494,6 +470,7 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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,26 +7,187 @@ 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.datasource.DataSource
|
||||||
|
import androidx.media3.datasource.HttpDataSource
|
||||||
|
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||||
|
import androidx.media3.datasource.cache.SimpleCache
|
||||||
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
|
import androidx.media3.exoplayer.offline.Download
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadManager
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadService
|
||||||
|
import androidx.media3.exoplayer.scheduler.Requirements
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
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.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.media.MediaType
|
import ani.dantotsu.okHttpClient
|
||||||
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 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.IOException
|
||||||
|
import java.util.concurrent.*
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
object Helper {
|
object Helper {
|
||||||
|
|
||||||
|
private var simpleCache: SimpleCache? = null
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||||
|
val dataSourceFactory = DataSource.Factory {
|
||||||
|
val dataSource: HttpDataSource =
|
||||||
|
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
|
defaultHeaders.forEach {
|
||||||
|
dataSource.setRequestProperty(it.key, it.value)
|
||||||
|
}
|
||||||
|
video.file.headers.forEach {
|
||||||
|
dataSource.setRequestProperty(it.key, it.value)
|
||||||
|
}
|
||||||
|
dataSource
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private var download: DownloadManager? = null
|
||||||
|
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
@UnstableApi
|
||||||
|
fun downloadManager(context: Context): DownloadManager {
|
||||||
|
return download ?: let {
|
||||||
|
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||||
|
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||||
|
val dataSourceFactory = DataSource.Factory {
|
||||||
|
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
val okHttpClient = networkHelper.client
|
||||||
|
val dataSource: HttpDataSource =
|
||||||
|
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
|
defaultHeaders.forEach {
|
||||||
|
dataSource.setRequestProperty(it.key, it.value)
|
||||||
|
}
|
||||||
|
dataSource
|
||||||
|
}
|
||||||
|
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||||
|
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||||
|
val downloadManager = DownloadManager(
|
||||||
|
context,
|
||||||
|
database,
|
||||||
|
getSimpleCache(context),
|
||||||
|
dataSourceFactory,
|
||||||
|
executorService
|
||||||
|
).apply {
|
||||||
|
requirements =
|
||||||
|
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||||
|
maxParallelDownloads = 3
|
||||||
|
}
|
||||||
|
downloadManager.addListener( //for testing
|
||||||
|
object : DownloadManager.Listener {
|
||||||
|
override fun onDownloadChanged(
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
download: Download,
|
||||||
|
finalException: Exception?
|
||||||
|
) {
|
||||||
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
|
Log.e("Downloader", "Download Completed")
|
||||||
|
} else if (download.state == Download.STATE_FAILED) {
|
||||||
|
Log.e("Downloader", "Download Failed")
|
||||||
|
} else if (download.state == Download.STATE_STOPPED) {
|
||||||
|
Log.e("Downloader", "Download Stopped")
|
||||||
|
} else if (download.state == Download.STATE_QUEUED) {
|
||||||
|
Log.e("Downloader", "Download Queued")
|
||||||
|
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||||
|
Log.e("Downloader", "Download Downloading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
downloadManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
@OptIn(UnstableApi::class)
|
||||||
fun startAnimeDownloadService(
|
fun startAnimeDownloadService(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -58,13 +219,22 @@ object Helper {
|
|||||||
|
|
||||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||||
val downloadCheck = downloadsManger
|
val downloadCheck = downloadsManger
|
||||||
.queryDownload(title, episode, MediaType.ANIME)
|
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
||||||
|
|
||||||
if (downloadCheck) {
|
if (downloadCheck) {
|
||||||
AlertDialog.Builder(context, R.style.MyPopup)
|
AlertDialog.Builder(context, R.style.MyPopup)
|
||||||
.setTitle("Download Exists")
|
.setTitle("Download Exists")
|
||||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||||
.setPositiveButton("Yes") { _, _ ->
|
.setPositiveButton("Yes") { _, _ ->
|
||||||
|
DownloadService.sendRemoveDownload(
|
||||||
|
context,
|
||||||
|
ExoplayerDownloadService::class.java,
|
||||||
|
PrefManager.getAnimeDownloadPreferences().getString(
|
||||||
|
animeDownloadTask.getTaskName(),
|
||||||
|
""
|
||||||
|
) ?: "",
|
||||||
|
false
|
||||||
|
)
|
||||||
PrefManager.getAnimeDownloadPreferences().edit()
|
PrefManager.getAnimeDownloadPreferences().edit()
|
||||||
.remove(animeDownloadTask.getTaskName())
|
.remove(animeDownloadTask.getTaskName())
|
||||||
.apply()
|
.apply()
|
||||||
@@ -72,15 +242,14 @@ object Helper {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
title,
|
title,
|
||||||
episode,
|
episode,
|
||||||
MediaType.ANIME
|
DownloadedType.Type.ANIME
|
||||||
)
|
)
|
||||||
) {
|
)
|
||||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
AnimeServiceDataSingleton.isServiceRunning = true
|
AnimeServiceDataSingleton.isServiceRunning = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton("No") { _, _ -> }
|
.setNegativeButton("No") { _, _ -> }
|
||||||
@@ -95,6 +264,18 @@ object Helper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun getSimpleCache(context: Context): SimpleCache {
|
||||||
|
return if (simpleCache == null) {
|
||||||
|
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||||
|
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||||
|
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
|
||||||
|
simpleCache!!
|
||||||
|
} else {
|
||||||
|
simpleCache!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
return ActivityCompat.checkSelfPermission(
|
return ActivityCompat.checkSelfPermission(
|
||||||
|
|||||||
@@ -207,21 +207,6 @@ 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) {
|
||||||
@@ -278,7 +263,7 @@ class AnimeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.loadTrending(1)
|
model.loadTrending(1)
|
||||||
model.loadAll()
|
model.loadUpdated()
|
||||||
model.loadPopular(
|
model.loadPopular(
|
||||||
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||||
PrefName.PopularAnimeList
|
PrefName.PopularAnimeList
|
||||||
@@ -298,9 +283,7 @@ class AnimeFragment : Fragment() {
|
|||||||
binding.root.requestApplyInsets()
|
binding.root.requestApplyInsets()
|
||||||
binding.root.requestLayout()
|
binding.root.requestLayout()
|
||||||
}
|
}
|
||||||
if (this::animePageAdapter.isInitialized && _binding != null) {
|
|
||||||
animePageAdapter.updateNotificationCount()
|
|
||||||
}
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,14 +4,12 @@ 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
|
||||||
@@ -23,13 +21,11 @@ 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
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.SearchActivity
|
import ani.dantotsu.media.SearchActivity
|
||||||
import ani.dantotsu.profile.ProfileActivity
|
|
||||||
import ani.dantotsu.px
|
import ani.dantotsu.px
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.setSlideIn
|
import ani.dantotsu.setSlideIn
|
||||||
@@ -44,7 +40,6 @@ 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
|
||||||
@@ -57,15 +52,14 @@ 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
|
||||||
trendingBinding = LayoutTrendingBinding.bind(binding.root)
|
trendingViewPager = binding.animeTrendingViewPager
|
||||||
trendingViewPager = trendingBinding.trendingViewPager
|
|
||||||
|
|
||||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
|
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
||||||
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.userAvatarContainer)
|
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
@@ -74,16 +68,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)
|
||||||
|
|
||||||
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
|
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = (-108f).px
|
bottomMargin = (-108f).px
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAvatar()
|
updateAvatar()
|
||||||
|
|
||||||
trendingBinding.searchBar.hint = "ANIME"
|
binding.animeSearchBar.hint = "ANIME"
|
||||||
trendingBinding.searchBarText.setOnClickListener {
|
binding.animeSearchBarText.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"),
|
||||||
@@ -91,28 +85,15 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
trendingBinding.userAvatar.setSafeOnClickListener {
|
binding.animeSearchBar.setEndIconOnClickListener {
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
trendingBinding.userAvatar.setOnLongClickListener { view ->
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
ContextCompat.startActivity(
|
|
||||||
view.context,
|
|
||||||
Intent(view.context, ProfileActivity::class.java)
|
|
||||||
.putExtra("userId", Anilist.userid), null
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
trendingBinding.searchBar.setEndIconOnClickListener {
|
|
||||||
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,
|
||||||
@@ -141,7 +122,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeIncludeList.isVisible = Anilist.userid != null
|
binding.animeIncludeList.visibility =
|
||||||
|
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
|
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
|
||||||
|
|
||||||
@@ -165,31 +147,30 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateTrending(adaptor: MediaAdaptor) {
|
fun updateTrending(adaptor: MediaAdaptor) {
|
||||||
trendingBinding.trendingProgressBar.visibility = View.GONE
|
binding.animeTrendingProgressBar.visibility = View.GONE
|
||||||
trendingBinding.trendingViewPager.adapter = adaptor
|
binding.animeTrendingViewPager.adapter = adaptor
|
||||||
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
binding.animeTrendingViewPager.offscreenPageLimit = 3
|
||||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
binding.animeTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||||
RecyclerView.OVER_SCROLL_NEVER
|
binding.animeTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||||
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
trendingBinding.trendingViewPager.currentItem += 1
|
binding.animeTrendingViewPager.currentItem =
|
||||||
|
binding.animeTrendingViewPager.currentItem + 1
|
||||||
}
|
}
|
||||||
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
|
binding.animeTrendingViewPager.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)
|
||||||
if (PrefManager.getVal(PrefName.TrendingScroller)) {
|
trendHandler!!.postDelayed(trendRun, 4000)
|
||||||
trendHandler!!.postDelayed(trendRun, 4000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
trendingBinding.trendingViewPager.layoutAnimation =
|
binding.animeTrendingViewPager.layoutAnimation =
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
binding.animeTitleContainer.startAnimation(setSlideUp())
|
||||||
binding.animeListContainer.layoutAnimation =
|
binding.animeListContainer.layoutAnimation =
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
binding.animeSeasonsCont.layoutAnimation =
|
binding.animeSeasonsCont.layoutAnimation =
|
||||||
@@ -197,83 +178,28 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateRecent(adaptor: MediaAdaptor) {
|
fun updateRecent(adaptor: MediaAdaptor) {
|
||||||
binding.apply {
|
binding.animeUpdatedProgressBar.visibility = View.GONE
|
||||||
init(
|
binding.animeUpdatedRecyclerView.adapter = adaptor
|
||||||
adaptor,
|
binding.animeUpdatedRecyclerView.layoutManager =
|
||||||
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(
|
||||||
recyclerView.context,
|
binding.animeUpdatedRecyclerView.context,
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
recyclerView.visibility = View.VISIBLE
|
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||||
title.visibility = View.VISIBLE
|
|
||||||
title.startAnimation(setSlideUp())
|
binding.animeRecently.visibility = View.VISIBLE
|
||||||
recyclerView.layoutAnimation =
|
binding.animeRecently.startAnimation(setSlideUp())
|
||||||
|
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) {
|
||||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
binding.animeUserAvatar.loadImage(Anilist.avatar)
|
||||||
trendingBinding.userAvatar.imageTintList = null
|
binding.animeUserAvatar.imageTintList = null
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNotificationCount() {
|
|
||||||
if (this::binding.isInitialized) {
|
|
||||||
trendingBinding.notificationCount.visibility =
|
|
||||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
|
||||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ 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
|
||||||
@@ -23,7 +21,6 @@ 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.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.blurImage
|
|
||||||
import ani.dantotsu.bottomBar
|
import ani.dantotsu.bottomBar
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||||
@@ -35,7 +32,6 @@ import ani.dantotsu.media.Media
|
|||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.user.ListActivity
|
import ani.dantotsu.media.user.ListActivity
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.profile.ProfileActivity
|
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.setSlideIn
|
import ani.dantotsu.setSlideIn
|
||||||
import ani.dantotsu.setSlideUp
|
import ani.dantotsu.setSlideUp
|
||||||
@@ -80,14 +76,9 @@ 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)
|
||||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
|
||||||
blurImage(
|
binding.homeUserBg.loadImage(Anilist.bg)
|
||||||
if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen,
|
|
||||||
Anilist.bg
|
|
||||||
)
|
|
||||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
|
||||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
|
||||||
|
|
||||||
binding.homeAnimeList.setOnClickListener {
|
binding.homeAnimeList.setOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
@@ -127,38 +118,26 @@ class HomeFragment : Fragment() {
|
|||||||
"dialog"
|
"dialog"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
binding.homeUserAvatarContainer.setOnLongClickListener {
|
|
||||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
ContextCompat.startActivity(
|
|
||||||
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
|
|
||||||
.putExtra("userId", Anilist.userid), null
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
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
|
||||||
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
|
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
|
||||||
|
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
reached = true
|
||||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||||
reached = true
|
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
.start()
|
||||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
} else {
|
||||||
|
if (reached) {
|
||||||
|
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||||
|
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||||
.start()
|
.start()
|
||||||
} else {
|
|
||||||
if (reached) {
|
|
||||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
|
||||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
|
||||||
.start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,7 +305,6 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val array = arrayOf(
|
val array = arrayOf(
|
||||||
"AnimeContinue",
|
"AnimeContinue",
|
||||||
"AnimeFav",
|
"AnimeFav",
|
||||||
@@ -382,10 +360,6 @@ 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) {
|
|
||||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
|
||||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
|
||||||
}
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,6 @@ import ani.dantotsu.openLinkInBrowser
|
|||||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
|
||||||
class LoginFragment : Fragment() {
|
class LoginFragment : Fragment() {
|
||||||
@@ -51,7 +50,7 @@ class LoginFragment : Fragment() {
|
|||||||
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
|
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
|
||||||
//.sani is encrypted, .ani is not
|
//.sani is encrypted, .ani is not
|
||||||
if (name.endsWith(".sani")) {
|
if (name.endsWith(".sani")) {
|
||||||
passwordAlertDialog { password ->
|
passwordAlertDialog() { password ->
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
val salt = jsonString.copyOfRange(0, 16)
|
val salt = jsonString.copyOfRange(0, 16)
|
||||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||||
@@ -79,7 +78,7 @@ class LoginFragment : Fragment() {
|
|||||||
toast("Invalid file type")
|
toast("Invalid file type")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log(e)
|
e.printStackTrace()
|
||||||
toast("Error importing settings")
|
toast("Error importing settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,37 +160,11 @@ class MangaFragment : Fragment() {
|
|||||||
})
|
})
|
||||||
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
|
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
|
||||||
if (i == true) {
|
if (i == true) {
|
||||||
model.getPopularNovel().observe(viewLifecycleOwner) {
|
model.getTrendingNovel().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) {
|
||||||
@@ -263,7 +237,7 @@ class MangaFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
model.loaded = true
|
model.loaded = true
|
||||||
model.loadTrending()
|
model.loadTrending()
|
||||||
model.loadAll()
|
model.loadTrendingNovel()
|
||||||
model.loadPopular(
|
model.loadPopular(
|
||||||
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||||
PrefName.PopularMangaList
|
PrefName.PopularMangaList
|
||||||
@@ -284,9 +258,6 @@ class MangaFragment : Fragment() {
|
|||||||
binding.root.requestApplyInsets()
|
binding.root.requestApplyInsets()
|
||||||
binding.root.requestLayout()
|
binding.root.requestLayout()
|
||||||
}
|
}
|
||||||
if (this::mangaPageAdapter.isInitialized && _binding != null) {
|
|
||||||
mangaPageAdapter.updateNotificationCount()
|
|
||||||
}
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ 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
|
||||||
@@ -23,12 +21,10 @@ 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
|
||||||
import ani.dantotsu.media.SearchActivity
|
import ani.dantotsu.media.SearchActivity
|
||||||
import ani.dantotsu.profile.ProfileActivity
|
|
||||||
import ani.dantotsu.px
|
import ani.dantotsu.px
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.setSlideIn
|
import ani.dantotsu.setSlideIn
|
||||||
@@ -43,7 +39,6 @@ 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
|
||||||
@@ -56,34 +51,32 @@ 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
|
||||||
trendingBinding = LayoutTrendingBinding.bind(binding.root)
|
trendingViewPager = binding.mangaTrendingViewPager
|
||||||
trendingViewPager = trendingBinding.trendingViewPager
|
|
||||||
|
|
||||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
|
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
||||||
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.userAvatarContainer)
|
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
val color = typedValue.data
|
val color = typedValue.data
|
||||||
|
|
||||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||||
|
|
||||||
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
|
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = (-108f).px
|
bottomMargin = (-108f).px
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAvatar()
|
updateAvatar()
|
||||||
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
|
||||||
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"),
|
||||||
@@ -91,23 +84,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
trendingBinding.userAvatar.setSafeOnClickListener {
|
binding.mangaUserAvatar.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")
|
||||||
}
|
}
|
||||||
trendingBinding.userAvatar.setOnLongClickListener { view ->
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
ContextCompat.startActivity(
|
|
||||||
view.context,
|
|
||||||
Intent(view.context, ProfileActivity::class.java)
|
|
||||||
.putExtra("userId", Anilist.userid), null
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
trendingBinding.searchBar.setEndIconOnClickListener {
|
binding.mangaSearchBar.setEndIconOnClickListener {
|
||||||
trendingBinding.searchBarText.performClick()
|
binding.mangaSearchBarText.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")
|
||||||
@@ -131,7 +115,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mangaIncludeList.isVisible = Anilist.userid != null
|
binding.mangaIncludeList.visibility =
|
||||||
|
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
|
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
|
||||||
|
|
||||||
@@ -153,121 +138,56 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateTrending(adaptor: MediaAdaptor) {
|
fun updateTrending(adaptor: MediaAdaptor) {
|
||||||
trendingBinding.trendingProgressBar.visibility = View.GONE
|
binding.mangaTrendingProgressBar.visibility = View.GONE
|
||||||
trendingBinding.trendingViewPager.adapter = adaptor
|
binding.mangaTrendingViewPager.adapter = adaptor
|
||||||
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
binding.mangaTrendingViewPager.offscreenPageLimit = 3
|
||||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
binding.mangaTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||||
RecyclerView.OVER_SCROLL_NEVER
|
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||||
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
trendingBinding.trendingViewPager.currentItem += 1
|
binding.mangaTrendingViewPager.currentItem =
|
||||||
|
binding.mangaTrendingViewPager.currentItem + 1
|
||||||
}
|
}
|
||||||
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
|
binding.mangaTrendingViewPager.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)
|
||||||
if (PrefManager.getVal(PrefName.TrendingScroller))
|
trendHandler!!.postDelayed(trendRun, 4000)
|
||||||
trendHandler!!.postDelayed(trendRun, 4000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
trendingBinding.trendingViewPager.layoutAnimation =
|
binding.mangaTrendingViewPager.layoutAnimation =
|
||||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||||
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
binding.mangaTitleContainer.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.apply {
|
binding.mangaNovelProgressBar.visibility = View.GONE
|
||||||
init(
|
binding.mangaNovelRecyclerView.adapter = adaptor
|
||||||
adaptor,
|
binding.mangaNovelRecyclerView.layoutManager =
|
||||||
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(
|
||||||
recyclerView.context,
|
binding.mangaNovelRecyclerView.context,
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
recyclerView.visibility = View.VISIBLE
|
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||||
title.visibility = View.VISIBLE
|
|
||||||
title.startAnimation(setSlideUp())
|
binding.mangaNovel.visibility = View.VISIBLE
|
||||||
recyclerView.layoutAnimation =
|
binding.mangaNovel.startAnimation(setSlideUp())
|
||||||
|
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) {
|
||||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
binding.mangaUserAvatar.loadImage(Anilist.avatar)
|
||||||
trendingBinding.userAvatar.imageTintList = null
|
binding.mangaUserAvatar.imageTintList = null
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNotificationCount() {
|
|
||||||
if (this::binding.isInitialized) {
|
|
||||||
trendingBinding.notificationCount.visibility =
|
|
||||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
|
||||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ package ani.dantotsu.media
|
|||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
data class Author(
|
data class Author(
|
||||||
var id: Int,
|
val id: String,
|
||||||
var name: String?,
|
val name: String,
|
||||||
var image: String?,
|
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
|
||||||
var role: String?,
|
|
||||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
|
|
||||||
var character: ArrayList<Character>? = null
|
|
||||||
) : Serializable
|
) : Serializable
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ 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
|
||||||
@@ -33,6 +32,7 @@ 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,15 +55,14 @@ 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
|
||||||
@@ -90,19 +89,9 @@ 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,58 +0,0 @@
|
|||||||
package ani.dantotsu.media
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.util.Pair
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import ani.dantotsu.databinding.ItemCharacterBinding
|
|
||||||
import ani.dantotsu.loadImage
|
|
||||||
import ani.dantotsu.setAnimation
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
class AuthorAdapter(
|
|
||||||
private val authorList: ArrayList<Author>,
|
|
||||||
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
|
|
||||||
val binding =
|
|
||||||
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
return AuthorViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
|
|
||||||
val binding = holder.binding
|
|
||||||
setAnimation(binding.root.context, holder.binding.root)
|
|
||||||
val author = authorList[position]
|
|
||||||
binding.itemCompactRelation.text = author.role
|
|
||||||
binding.itemCompactImage.loadImage(author.image)
|
|
||||||
binding.itemCompactTitle.text = author.name
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = authorList.size
|
|
||||||
inner class AuthorViewHolder(val binding: ItemCharacterBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
init {
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
val author = authorList[bindingAdapterPosition]
|
|
||||||
ContextCompat.startActivity(
|
|
||||||
itemView.context,
|
|
||||||
Intent(
|
|
||||||
itemView.context,
|
|
||||||
AuthorActivity::class.java
|
|
||||||
).putExtra("author", author as Serializable),
|
|
||||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
|
||||||
itemView.context as Activity,
|
|
||||||
Pair.create(
|
|
||||||
binding.itemCompactImage,
|
|
||||||
ViewCompat.getTransitionName(binding.itemCompactImage)!!
|
|
||||||
),
|
|
||||||
).toBundle()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
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
|
||||||
@@ -14,8 +16,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
|
||||||
@@ -32,6 +34,7 @@ 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)
|
||||||
|
|
||||||
@@ -42,6 +45,13 @@ 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
|
||||||
@@ -64,16 +74,20 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
binding.root.fitsSystemWindows = false
|
binding.root.fitsSystemWindows = false
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
hideSystemBarsExtendView()
|
window.setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
|
)
|
||||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
topMargin = statusBarHeight
|
topMargin = statusBarHeight
|
||||||
|
bottomMargin = navBarHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
binding.listTitle.setText(R.string.release_calendar)
|
binding.listTitle.setText(R.string.release_calendar)
|
||||||
binding.listSort.visibility = View.GONE
|
binding.listSort.visibility = View.GONE
|
||||||
binding.random.visibility = View.GONE
|
|
||||||
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ data class Character(
|
|||||||
val image: String?,
|
val image: String?,
|
||||||
val banner: String?,
|
val banner: String?,
|
||||||
val role: String,
|
val role: String,
|
||||||
var isFav: Boolean,
|
|
||||||
var description: String? = null,
|
var description: String? = null,
|
||||||
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,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -23,13 +24,12 @@ 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]
|
||||||
val whitespace = "${character.role} "
|
binding.itemCompactRelation.text = 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -8,7 +7,6 @@ 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
|
||||||
@@ -17,25 +15,20 @@ import androidx.recyclerview.widget.ConcatAdapter
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
|
||||||
import ani.dantotsu.connections.anilist.AnilistMutations
|
|
||||||
import ani.dantotsu.databinding.ActivityCharacterBinding
|
import ani.dantotsu.databinding.ActivityCharacterBinding
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.openLinkInBrowser
|
|
||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.px
|
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.snackString
|
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||||
@@ -55,7 +48,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
initActivity(this)
|
initActivity(this)
|
||||||
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||||
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
|
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
|
||||||
ContextCompat.getColor(this, R.color.transparent)
|
ContextCompat.getColor(this, R.color.status)
|
||||||
|
|
||||||
val banner =
|
val banner =
|
||||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
|
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
|
||||||
@@ -82,40 +75,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
character.image
|
character.image
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val link = "https://anilist.co/character/${character.id}"
|
|
||||||
binding.characterShare.setOnClickListener {
|
|
||||||
val i = Intent(Intent.ACTION_SEND)
|
|
||||||
i.type = "text/plain"
|
|
||||||
i.putExtra(Intent.EXTRA_TEXT, link)
|
|
||||||
startActivity(Intent.createChooser(i, character.name))
|
|
||||||
}
|
|
||||||
binding.characterShare.setOnLongClickListener {
|
|
||||||
openLinkInBrowser(link)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
character.isFav =
|
|
||||||
Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
binding.characterFav.setImageResource(
|
|
||||||
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.characterFav.setOnClickListener {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, character.id)) {
|
|
||||||
character.isFav = !character.isFav
|
|
||||||
binding.characterFav.setImageResource(
|
|
||||||
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
snackString("Failed to toggle favorite")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model.getCharacter().observe(this) {
|
model.getCharacter().observe(this) {
|
||||||
if (it != null && !loaded) {
|
if (it != null && !loaded) {
|
||||||
character = it
|
character = it
|
||||||
@@ -154,7 +114,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
binding.characterProgress.isGone = loaded
|
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,11 +139,13 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
isCollapsed = true
|
isCollapsed = true
|
||||||
if (immersiveMode) this.window.statusBarColor =
|
if (immersiveMode) this.window.statusBarColor =
|
||||||
ContextCompat.getColor(this, R.color.nav_bg)
|
ContextCompat.getColor(this, R.color.nav_bg)
|
||||||
|
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
||||||
}
|
}
|
||||||
if (percentage <= percent && isCollapsed) {
|
if (percentage <= percent && isCollapsed) {
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
if (immersiveMode) this.window.statusBarColor =
|
if (immersiveMode) this.window.statusBarColor =
|
||||||
ContextCompat.getColor(this, R.color.transparent)
|
ContextCompat.getColor(this, R.color.status)
|
||||||
|
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
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
|
||||||
@@ -21,36 +20,23 @@ 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() != "")
|
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
||||||
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
|
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||||
(if (character.gender != "null")
|
"Male" -> currActivity()!!.getString(R.string.male)
|
||||||
currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
"Female" -> currActivity()!!.getString(R.string.female)
|
||||||
currActivity()!!.getString(R.string.male) -> currActivity()!!.getString(
|
else -> character.gender
|
||||||
R.string.male
|
} else "") + "\n" + character.description
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
||||||
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
|
||||||
|
|||||||
@@ -67,12 +67,11 @@ class GenreActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun loadLocalGenres(): ArrayList<String>? {
|
private fun loadLocalGenres(): ArrayList<String>? {
|
||||||
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||||
.toMutableList()
|
.toMutableList() as ArrayList<String>?
|
||||||
return if (genres.isEmpty()) {
|
return if (genres.isNullOrEmpty()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
//sort alphabetically
|
genres
|
||||||
genres.sort().let { genres as ArrayList<String> }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,6 @@ 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
|
||||||
|
|
||||||
@@ -26,7 +25,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 favourites: Int? = null,
|
var popularity: Int? = null,
|
||||||
|
|
||||||
var isAdult: Boolean,
|
var isAdult: Boolean,
|
||||||
var isFav: Boolean = false,
|
var isFav: Boolean = false,
|
||||||
@@ -57,17 +56,13 @@ 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 prequel: Media? = null,
|
var prequel: Media? = null,
|
||||||
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,
|
||||||
|
|
||||||
@@ -87,7 +82,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 ?: apiMedia.coverImage?.medium,
|
cover = apiMedia.coverImage?.large,
|
||||||
banner = apiMedia.bannerImage,
|
banner = apiMedia.bannerImage,
|
||||||
status = apiMedia.status.toString(),
|
status = apiMedia.status.toString(),
|
||||||
isFav = apiMedia.isFavourite!!,
|
isFav = apiMedia.isFavourite!!,
|
||||||
@@ -99,8 +94,6 @@ 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)
|
||||||
@@ -115,8 +108,6 @@ 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
|
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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
|
||||||
@@ -13,25 +15,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.R
|
import ani.dantotsu.*
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +43,6 @@ class MediaAdaptor(
|
|||||||
private val activity: FragmentActivity,
|
private val activity: FragmentActivity,
|
||||||
private val matchParent: Boolean = false,
|
private val matchParent: Boolean = false,
|
||||||
private val viewPager: ViewPager2? = null,
|
private val viewPager: ViewPager2? = null,
|
||||||
private val fav: Boolean = false,
|
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
@@ -83,7 +84,7 @@ class MediaAdaptor(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (type) {
|
when (type) {
|
||||||
0 -> {
|
0 -> {
|
||||||
@@ -92,8 +93,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.isVisible =
|
b.itemCompactOngoing.visibility =
|
||||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore
|
((if (media.userScore == 0) (media.meanScore
|
||||||
@@ -127,7 +128,6 @@ class MediaAdaptor(
|
|||||||
)
|
)
|
||||||
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
|
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
|
||||||
}
|
}
|
||||||
b.itemCompactProgressContainer.visibility = if (fav) View.GONE else View.VISIBLE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +137,9 @@ class MediaAdaptor(
|
|||||||
val media = mediaList?.get(position)
|
val media = mediaList?.get(position)
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
b.itemCompactImage.loadImage(media.cover)
|
b.itemCompactImage.loadImage(media.cover)
|
||||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
|
||||||
b.itemCompactOngoing.isVisible =
|
b.itemCompactOngoing.visibility =
|
||||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.itemCompactTitle.text = media.userPreferredName
|
b.itemCompactTitle.text = media.userPreferredName
|
||||||
b.itemCompactScore.text =
|
b.itemCompactScore.text =
|
||||||
((if (media.userScore == 0) (media.meanScore
|
((if (media.userScore == 0) (media.meanScore
|
||||||
@@ -149,30 +149,25 @@ 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) {
|
||||||
val itemTotal = " " + if ((media.anime.totalEpisodes
|
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||||
?: 0) != 1
|
?: 0) != 1
|
||||||
) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(
|
) currActivity()!!.getString(R.string.episode_plural)
|
||||||
R.string.episode_singular
|
else currActivity()!!.getString(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) {
|
||||||
val itemTotal = " " + if ((media.manga.totalChapters
|
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||||
?: 0) != 1
|
?: 0) != 1
|
||||||
) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(
|
) currActivity()!!.getString(R.string.chapter_plural)
|
||||||
R.string.chapter_singular
|
else currActivity()!!.getString(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)
|
||||||
val end = mediaList.size - start
|
notifyDataSetChanged()
|
||||||
notifyItemRangeInserted(start, end)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +176,6 @@ 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)
|
||||||
@@ -191,12 +185,17 @@ class MediaAdaptor(
|
|||||||
AccelerateDecelerateInterpolator()
|
AccelerateDecelerateInterpolator()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
blurImage(
|
val banner =
|
||||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
|
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||||
media.banner ?: media.cover
|
val context = b.itemCompactBanner.context
|
||||||
)
|
if (!(context as Activity).isDestroyed)
|
||||||
b.itemCompactOngoing.isVisible =
|
Glide.with(context as Context)
|
||||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
.load(GlideUrl(media.banner ?: media.cover))
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||||
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||||
|
.into(banner)
|
||||||
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.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
|
||||||
@@ -243,12 +242,17 @@ class MediaAdaptor(
|
|||||||
AccelerateDecelerateInterpolator()
|
AccelerateDecelerateInterpolator()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
blurImage(
|
val banner =
|
||||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
|
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||||
media.banner ?: media.cover
|
val context = b.itemCompactBanner.context
|
||||||
)
|
if (!(context as Activity).isDestroyed)
|
||||||
b.itemCompactOngoing.isVisible =
|
Glide.with(context as Context)
|
||||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
.load(GlideUrl(media.banner ?: media.cover))
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||||
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||||
|
.into(banner)
|
||||||
|
b.itemCompactOngoing.visibility =
|
||||||
|
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||||
b.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
|
||||||
|
|||||||
@@ -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.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
|
||||||
@@ -13,40 +12,34 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.ImageView
|
import 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.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
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import ani.dantotsu.CustomBottomNavBar
|
||||||
import ani.dantotsu.GesturesListener
|
import ani.dantotsu.GesturesListener
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.ZoomOutPageTransformer
|
import ani.dantotsu.ZoomOutPageTransformer
|
||||||
import ani.dantotsu.blurImage
|
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.copyToClipboard
|
import ani.dantotsu.copyToClipboard
|
||||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||||
import ani.dantotsu.media.comments.CommentsFragment
|
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment
|
import ani.dantotsu.media.manga.MangaReadFragment
|
||||||
import ani.dantotsu.media.novel.NovelReadFragment
|
import ani.dantotsu.media.novel.NovelReadFragment
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
import ani.dantotsu.others.AndroidBug5497Workaround
|
|
||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
@@ -54,49 +47,35 @@ 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 com.google.android.material.navigation.NavigationBarView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
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.runBlocking
|
|
||||||
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
|
private lateinit var binding: ActivityMediaBinding
|
||||||
private val scope = lifecycleScope
|
private val scope = lifecycleScope
|
||||||
private val model: MediaDetailsViewModel by viewModels()
|
private val model: MediaDetailsViewModel by viewModels()
|
||||||
|
private lateinit var tabLayout: NavigationBarView
|
||||||
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("ClickableViewAccessibility")
|
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||||
val id = intent.getIntExtra("mediaId", -1)
|
|
||||||
if (id != -1) {
|
|
||||||
runBlocking {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
media = Anilist.query.getMedia(id, false) ?: emptyMedia()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (media.name == "No media found") {
|
if (media.name == "No media found") {
|
||||||
snackString(media.name)
|
snackString(media.name)
|
||||||
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
|
||||||
@@ -104,44 +83,21 @@ 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
|
|
||||||
|
|
||||||
// Ui init
|
//Ui init
|
||||||
|
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = navBarHeight
|
|
||||||
}
|
|
||||||
val oldMargin = binding.mediaViewPager.marginBottom
|
|
||||||
AndroidBug5497Workaround.assistActivity(this) {
|
|
||||||
if (it) {
|
|
||||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = 0
|
|
||||||
}
|
|
||||||
navBar.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = oldMargin
|
|
||||||
}
|
|
||||||
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 }
|
||||||
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||||
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
||||||
|
|
||||||
|
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
bottomMargin = navBarHeight
|
||||||
|
}
|
||||||
|
|
||||||
binding.mediaTitle.isSelected = true
|
binding.mediaTitle.isSelected = true
|
||||||
|
|
||||||
mMaxScrollSize = binding.mediaAppBar.totalScrollRange
|
mMaxScrollSize = binding.mediaAppBar.totalScrollRange
|
||||||
@@ -163,6 +119,7 @@ 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 NavigationBarView
|
||||||
viewPager.isUserInputEnabled = false
|
viewPager.isUserInputEnabled = false
|
||||||
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||||
|
|
||||||
@@ -172,15 +129,13 @@ 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,
|
||||||
coverTitle,
|
media.userPreferredName + "[Cover]",
|
||||||
media.cover
|
media.cover
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
banner.loadImage(media.banner ?: media.cover, 400)
|
||||||
blurImage(banner, media.banner ?: media.cover)
|
|
||||||
val gestureDetector = GestureDetector(this, object : GesturesListener() {
|
val gestureDetector = GestureDetector(this, object : GesturesListener() {
|
||||||
override fun onDoubleClick(event: MotionEvent) {
|
override fun onDoubleClick(event: MotionEvent) {
|
||||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
|
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
|
||||||
@@ -192,10 +147,9 @@ 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,
|
||||||
bannerTitle,
|
media.userPreferredName + "[Banner]",
|
||||||
media.banner ?: media.cover
|
media.banner ?: media.cover
|
||||||
)
|
)
|
||||||
banner.performClick()
|
banner.performClick()
|
||||||
@@ -203,8 +157,7 @@ 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)) {
|
||||||
val mediaTitle = " ${media.userPreferredName}"
|
binding.mediaTitle.text = " ${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
|
||||||
@@ -228,6 +181,20 @@ 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,
|
||||||
@@ -235,7 +202,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,
|
R.color.violet_400,//TODO: Change to colorSecondary
|
||||||
media.isFav
|
media.isFav
|
||||||
) {
|
) {
|
||||||
media.isFav = it
|
media.isFav = it
|
||||||
@@ -250,13 +217,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
@SuppressLint("ResourceType")
|
@SuppressLint("ResourceType")
|
||||||
fun total() {
|
fun total() {
|
||||||
val text = SpannableStringBuilder().apply {
|
val text = SpannableStringBuilder().apply {
|
||||||
val mediaTypedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||||
com.google.android.material.R.attr.colorOnBackground,
|
com.google.android.material.R.attr.colorOnBackground,
|
||||||
mediaTypedValue,
|
typedValue,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
val white = mediaTypedValue.data
|
val white = typedValue.data
|
||||||
if (media.userStatus != null) {
|
if (media.userStatus != null) {
|
||||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
@@ -346,68 +313,49 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
progress()
|
progress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adult = media.isAdult
|
adult = media.isAdult
|
||||||
|
tabLayout.menu.clear()
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
viewPager.adapter =
|
viewPager.adapter =
|
||||||
ViewPagerAdapter(
|
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
|
||||||
supportFragmentManager,
|
tabLayout.inflateMenu(R.menu.anime_menu_detail)
|
||||||
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,
|
||||||
lifecycle,
|
lifecycle,
|
||||||
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA,
|
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
|
||||||
media,
|
|
||||||
intent.getIntExtra("commentId", -1)
|
|
||||||
)
|
)
|
||||||
|
if (media.format == "NOVEL") {
|
||||||
|
tabLayout.inflateMenu(R.menu.novel_menu_detail)
|
||||||
|
} else {
|
||||||
|
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||||
|
}
|
||||||
anime = false
|
anime = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
selected = media.selected!!.window
|
selected = media.selected!!.window
|
||||||
binding.mediaTitle.translationX = -screenWidth
|
binding.mediaTitle.translationX = -screenWidth
|
||||||
|
tabLayout.visibility = View.VISIBLE
|
||||||
|
|
||||||
val infoTab = navBar.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
|
tabLayout.setOnItemSelectedListener { item ->
|
||||||
val watchTab = if (anime) {
|
selectFromID(item.itemId)
|
||||||
navBar.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
|
viewPager.setCurrentItem(selected, false)
|
||||||
} else if (media.format == "NOVEL") {
|
val sel = model.loadSelected(media, isDownload)
|
||||||
navBar.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
|
sel.window = selected
|
||||||
} else {
|
model.saveSelected(media.id, sel)
|
||||||
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
|
true
|
||||||
}
|
}
|
||||||
val commentTab =
|
|
||||||
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
|
|
||||||
navBar.addTab(infoTab)
|
tabLayout.selectedItemId = idFromSelect()
|
||||||
navBar.addTab(watchTab)
|
viewPager.setCurrentItem(selected, false)
|
||||||
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
|
||||||
}
|
}
|
||||||
if (intent.getStringExtra("FRAGMENT_TO_LOAD") != null) selected = 2
|
|
||||||
if (viewPager.currentItem != selected) viewPager.post {
|
|
||||||
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) {
|
||||||
@@ -420,21 +368,35 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
private fun selectFromID(id: Int) {
|
||||||
val rightMargin = if (resources.configuration.orientation ==
|
when (id) {
|
||||||
Configuration.ORIENTATION_LANDSCAPE
|
R.id.info -> {
|
||||||
) navBarHeight else 0
|
selected = 0
|
||||||
val bottomMargin = if (resources.configuration.orientation ==
|
}
|
||||||
Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
) 0 else navBarHeight
|
R.id.watch, R.id.read -> {
|
||||||
val params: ViewGroup.MarginLayoutParams =
|
selected = 1
|
||||||
navBar.layoutParams as ViewGroup.MarginLayoutParams
|
}
|
||||||
params.updateMargins(right = rightMargin, bottom = bottomMargin)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun idFromSelect(): Int {
|
||||||
|
if (anime) when (selected) {
|
||||||
|
0 -> return R.id.info
|
||||||
|
1 -> return R.id.watch
|
||||||
|
}
|
||||||
|
else when (selected) {
|
||||||
|
0 -> return R.id.info
|
||||||
|
1 -> return R.id.read
|
||||||
|
}
|
||||||
|
return R.id.info
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
navBar.selectTabAt(selected)
|
if (this::tabLayout.isInitialized) {
|
||||||
|
tabLayout.selectedItemId = idFromSelect()
|
||||||
|
}
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,36 +404,24 @@ 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,
|
||||||
private val mediaType: SupportedMedia,
|
private val media: SupportedMedia
|
||||||
private val media: Media,
|
|
||||||
private val commentId: Int
|
|
||||||
) :
|
) :
|
||||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||||
|
|
||||||
override fun getItemCount(): Int = 3
|
override fun getItemCount(): Int = 2
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = when (position) {
|
override fun createFragment(position: Int): Fragment = when (position) {
|
||||||
0 -> MediaInfoFragment()
|
0 -> MediaInfoFragment()
|
||||||
1 -> when (mediaType) {
|
1 -> when (media) {
|
||||||
SupportedMedia.ANIME -> AnimeWatchFragment()
|
SupportedMedia.ANIME -> AnimeWatchFragment()
|
||||||
SupportedMedia.MANGA -> MangaReadFragment()
|
SupportedMedia.MANGA -> MangaReadFragment()
|
||||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> {
|
|
||||||
val fragment = CommentsFragment()
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putInt("mediaId", media.id)
|
|
||||||
bundle.putString("mediaName", media.mainName())
|
|
||||||
if (commentId != -1) bundle.putInt("commentId", commentId)
|
|
||||||
fragment.arguments = bundle
|
|
||||||
fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> MediaInfoFragment()
|
else -> MediaInfoFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,6 +439,13 @@ 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)
|
||||||
@@ -527,7 +484,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
private val c1: Int,
|
private val c1: Int,
|
||||||
private val c2: Int,
|
private val c2: Int,
|
||||||
var clicked: Boolean,
|
var clicked: Boolean,
|
||||||
needsInitialClick: Boolean = false,
|
|
||||||
callback: suspend (Boolean) -> (Unit)
|
callback: suspend (Boolean) -> (Unit)
|
||||||
) {
|
) {
|
||||||
private var disabled = false
|
private var disabled = false
|
||||||
@@ -536,11 +492,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
enabled(true)
|
enabled(true)
|
||||||
if (needsInitialClick) {
|
|
||||||
scope.launch {
|
|
||||||
clicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
image.setOnClickListener {
|
image.setOnClickListener {
|
||||||
if (pressable && !disabled) {
|
if (pressable && !disabled) {
|
||||||
pressable = false
|
pressable = false
|
||||||
@@ -595,4 +546,5 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
companion object {
|
companion object {
|
||||||
var mediaSingleton: Media? = null
|
var mediaSingleton: Media? = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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.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
|
||||||
@@ -28,7 +29,6 @@ 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,23 +52,26 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
if (isDownload) {
|
if (isDownload) {
|
||||||
data.sourceIndex = when {
|
data.sourceIndex = if (media.anime != null) {
|
||||||
media.anime != null -> {
|
AnimeSources.list.size - 1
|
||||||
AnimeSources.list.size - 1
|
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
||||||
}
|
MangaSources.list.size - 1
|
||||||
|
} else {
|
||||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
|
NovelSources.list.size - 1
|
||||||
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
|
||||||
|
|
||||||
@@ -149,10 +152,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) { extractor ->
|
loadByVideoServers(link, ep.extra, it) {
|
||||||
if (extractor.videos.isNotEmpty()) {
|
if (it.videos.isNotEmpty()) {
|
||||||
list.add(extractor)
|
list.add(it)
|
||||||
ep.extractorCallback?.invoke(extractor)
|
ep.extractorCallback?.invoke(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +223,7 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setEpisode(ep: Episode?, who: String) {
|
fun setEpisode(ep: Episode?, who: String) {
|
||||||
Logger.log("set episode ${ep?.number} - $who")
|
logger("set episode ${ep?.number} - $who", false)
|
||||||
episode.postValue(ep)
|
episode.postValue(ep)
|
||||||
MainScope().launch(Dispatchers.Main) {
|
MainScope().launch(Dispatchers.Main) {
|
||||||
episode.value = null
|
episode.value = null
|
||||||
@@ -267,7 +270,7 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
mangaChapters
|
mangaChapters
|
||||||
|
|
||||||
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
||||||
Logger.log("Loading Manga Chapters : $mangaLoaded")
|
logger("Loading Manga Chapters : $mangaLoaded")
|
||||||
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
||||||
mangaLoaded[i] =
|
mangaLoaded[i] =
|
||||||
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||||
@@ -288,6 +291,7 @@ 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 {
|
||||||
|
|
||||||
|
|||||||
@@ -15,33 +15,16 @@ 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.R
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||||
import ani.dantotsu.copyToClipboard
|
import ani.dantotsu.databinding.*
|
||||||
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
|
||||||
@@ -53,6 +36,7 @@ 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!!
|
||||||
@@ -61,8 +45,6 @@ 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?,
|
||||||
@@ -80,8 +62,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.isGone = loaded
|
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||||
binding.mediaInfoContainer.isVisible = loaded
|
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
||||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||||
|
|
||||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||||
@@ -91,19 +73,16 @@ class MediaInfoFragment : Fragment() {
|
|||||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||||
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
|
||||||
val infoName = tripleTab + (media.name ?: media.nameRomaji)
|
binding.mediaInfoName.text = "\t\t\t" + (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
|
||||||
val infoNameRomanji = tripleTab + media.nameRomaji
|
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
||||||
binding.mediaInfoNameRomaji.text = infoNameRomanji
|
|
||||||
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
||||||
copyToClipboard(media.nameRomaji)
|
copyToClipboard(media.nameRomaji)
|
||||||
true
|
true
|
||||||
@@ -115,8 +94,6 @@ 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
|
||||||
|
|
||||||
@@ -145,10 +122,8 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||||
val seasonInfo =
|
binding.mediaInfoSeason.text =
|
||||||
"${(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
|
||||||
@@ -182,12 +157,9 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||||
val infoTotal = if (media.anime.nextAiringEpisode != null)
|
binding.mediaInfoTotal.text =
|
||||||
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
|
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
|
||||||
else
|
?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString()
|
||||||
(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)
|
||||||
@@ -214,10 +186,8 @@ 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
|
||||||
)
|
)
|
||||||
val infoDesc =
|
binding.mediaInfoDescription.text =
|
||||||
tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
|
"\t\t\t" + 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)
|
||||||
@@ -227,7 +197,8 @@ 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 }
|
||||||
|
|
||||||
@@ -437,157 +408,101 @@ class MediaInfoFragment : Fragment() {
|
|||||||
parent.addView(bind.root)
|
parent.addView(bind.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||||
|
val bind = ItemTitleRecyclerBinding.inflate(
|
||||||
|
LayoutInflater.from(context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
bind.itemTitle.setText(R.string.characters)
|
||||||
|
bind.itemRecycler.adapter =
|
||||||
|
CharacterAdapter(media.characters!!)
|
||||||
|
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
LinearLayoutManager.HORIZONTAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
parent.addView(bind.root)
|
||||||
|
}
|
||||||
|
|
||||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||||
if (media.sequel != null || media.prequel != null) {
|
if (media.sequel != null || media.prequel != null) {
|
||||||
ItemQuelsBinding.inflate(
|
val bind = ItemQuelsBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
).apply {
|
)
|
||||||
|
|
||||||
if (media.sequel != null) {
|
if (media.sequel != null) {
|
||||||
mediaInfoSequel.visibility = View.VISIBLE
|
bind.mediaInfoSequel.visibility = View.VISIBLE
|
||||||
mediaInfoSequelImage.loadImage(
|
bind.mediaInfoSequelImage.loadImage(
|
||||||
media.sequel!!.banner ?: media.sequel!!.cover
|
media.sequel!!.banner ?: media.sequel!!.cover
|
||||||
)
|
)
|
||||||
mediaInfoSequel.setSafeOnClickListener {
|
bind.mediaInfoSequel.setSafeOnClickListener {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
|
requireContext(),
|
||||||
|
Intent(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
Intent(
|
MediaDetailsActivity::class.java
|
||||||
requireContext(),
|
).putExtra(
|
||||||
MediaDetailsActivity::class.java
|
"media",
|
||||||
).putExtra(
|
media.sequel as Serializable
|
||||||
"media",
|
), null
|
||||||
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(
|
}
|
||||||
|
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(),
|
||||||
Intent(
|
MediaDetailsActivity::class.java
|
||||||
requireContext(),
|
).putExtra(
|
||||||
MediaDetailsActivity::class.java
|
"media",
|
||||||
).putExtra(
|
media.prequel as Serializable
|
||||||
"media",
|
), null
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemTitleRecyclerBinding.inflate(
|
val bindi = ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
).apply {
|
)
|
||||||
|
|
||||||
itemRecycler.adapter =
|
bindi.itemRecycler.adapter =
|
||||||
MediaAdaptor(0, media.relations!!, requireActivity())
|
MediaAdaptor(0, media.relations!!, requireActivity())
|
||||||
itemRecycler.layoutManager = LinearLayoutManager(
|
bindi.itemRecycler.layoutManager = LinearLayoutManager(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
LinearLayoutManager.HORIZONTAL,
|
LinearLayoutManager.HORIZONTAL,
|
||||||
false
|
|
||||||
)
|
|
||||||
parent.addView(root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
|
||||||
ItemTitleRecyclerBinding.inflate(
|
|
||||||
LayoutInflater.from(context),
|
|
||||||
parent,
|
|
||||||
false
|
false
|
||||||
).apply {
|
)
|
||||||
itemTitle.setText(R.string.characters)
|
parent.addView(bindi.root)
|
||||||
itemRecycler.adapter =
|
|
||||||
CharacterAdapter(media.characters!!)
|
|
||||||
itemRecycler.layoutManager = LinearLayoutManager(
|
|
||||||
requireContext(),
|
|
||||||
LinearLayoutManager.HORIZONTAL,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
parent.addView(root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!media.staff.isNullOrEmpty() && !offline) {
|
|
||||||
ItemTitleRecyclerBinding.inflate(
|
|
||||||
LayoutInflater.from(context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
).apply {
|
|
||||||
itemTitle.setText(R.string.staff)
|
|
||||||
itemRecycler.adapter =
|
|
||||||
AuthorAdapter(media.staff!!)
|
|
||||||
itemRecycler.layoutManager = LinearLayoutManager(
|
|
||||||
requireContext(),
|
|
||||||
LinearLayoutManager.HORIZONTAL,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
parent.addView(root)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
||||||
ItemTitleRecyclerBinding.inflate(
|
val bind = ItemTitleRecyclerBinding.inflate(
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
).apply {
|
)
|
||||||
itemTitle.setText(R.string.recommended)
|
bind.itemTitle.setText(R.string.recommended)
|
||||||
itemRecycler.adapter =
|
bind.itemRecycler.adapter =
|
||||||
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
||||||
itemRecycler.layoutManager = LinearLayoutManager(
|
bind.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 {
|
)
|
||||||
itemTitle.setText(R.string.social)
|
parent.addView(bind.root)
|
||||||
itemRecycler.adapter =
|
|
||||||
MediaSocialAdapter(media.users!!)
|
|
||||||
itemRecycler.layoutManager = LinearLayoutManager(
|
|
||||||
requireContext(),
|
|
||||||
LinearLayoutManager.HORIZONTAL,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
parent.addView(root)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,12 +527,11 @@ class MediaInfoFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onViewCreated(view, null)
|
super.onViewCreated(view, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
binding.mediaInfoProgressBar.isGone = loaded
|
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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,18 +11,11 @@ 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.BottomSheetDialogFragment
|
import ani.dantotsu.*
|
||||||
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
|
||||||
@@ -42,6 +36,7 @@ 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?
|
||||||
@@ -173,10 +168,9 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
val init =
|
val init =
|
||||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||||
.toInt() else 0
|
.toInt() else 0
|
||||||
if (init < (total ?: 5000)) {
|
if (init < (total
|
||||||
val progressText = "${init + 1}"
|
?: 5000)
|
||||||
binding.mediaListProgress.setText(progressText)
|
) binding.mediaListProgress.setText((init + 1).toString())
|
||||||
}
|
|
||||||
if (init + 1 == (total ?: 5000)) {
|
if (init + 1 == (total ?: 5000)) {
|
||||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||||
onComplete()
|
onComplete()
|
||||||
@@ -260,28 +254,20 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.mediaListDelete.setOnClickListener {
|
binding.mediaListDelete.setOnClickListener {
|
||||||
var id = media!!.userListId
|
val id = media!!.userListId
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
if (id != null) {
|
|
||||||
Anilist.mutation.deleteList(id!!)
|
|
||||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
|
||||||
} else {
|
|
||||||
val profile = Anilist.query.userMediaDetails(media!!)
|
|
||||||
profile.userListId?.let { listId ->
|
|
||||||
id = listId
|
|
||||||
Anilist.mutation.deleteList(listId)
|
|
||||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
Refresh.all()
|
scope.launch {
|
||||||
snackString(getString(R.string.deleted_from_list))
|
withContext(Dispatchers.IO) {
|
||||||
dismissAllowingStateLoss()
|
Anilist.mutation.deleteList(id)
|
||||||
|
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||||
|
}
|
||||||
|
Refresh.all()
|
||||||
|
snackString(getString(R.string.deleted_from_list))
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
snackString(getString(R.string.no_list_id))
|
snackString(getString(R.string.no_list_id))
|
||||||
|
Refresh.all()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -9,16 +10,11 @@ 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.BottomSheetDialogFragment
|
import ani.dantotsu.*
|
||||||
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
|
||||||
@@ -58,43 +54,10 @@ 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
|
||||||
binding.mediaListDelete.setOnClickListener {
|
|
||||||
var id = media.userListId
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
if (id != null) {
|
|
||||||
try {
|
|
||||||
Anilist.mutation.deleteList(id!!)
|
|
||||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
snackString(getString(R.string.delete_fail_reason, e.message))
|
|
||||||
}
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val profile = Anilist.query.userMediaDetails(media)
|
|
||||||
profile.userListId?.let { listId ->
|
|
||||||
id = listId
|
|
||||||
Anilist.mutation.deleteList(listId)
|
|
||||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (id != null) {
|
|
||||||
Refresh.all()
|
|
||||||
snackString(getString(R.string.deleted_from_list))
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
} else {
|
|
||||||
snackString(getString(R.string.no_list_id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.mediaListProgressBar.visibility = View.GONE
|
binding.mediaListProgressBar.visibility = View.GONE
|
||||||
binding.mediaListLayout.visibility = View.VISIBLE
|
binding.mediaListLayout.visibility = View.VISIBLE
|
||||||
@@ -157,10 +120,7 @@ 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)) {
|
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
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 {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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(curr - 86400, curr + (86400 * 6))
|
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
|
||||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||||
val map = mutableMapOf<String, MutableList<Media>>()
|
val map = mutableMapOf<String, MutableList<Media>>()
|
||||||
val idMap = mutableMapOf<String, MutableList<Int>>()
|
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
|||||||
return ProgressViewHolder(binding)
|
return ProgressViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("SetTextI18n", "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,30 +4,24 @@ 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.Timer
|
import java.util.*
|
||||||
import java.util.TimerTask
|
|
||||||
|
|
||||||
class SearchActivity : AppCompatActivity() {
|
class SearchActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivitySearchBinding
|
private lateinit var binding: ActivitySearchBinding
|
||||||
@@ -70,18 +64,11 @@ 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 = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")
|
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
|
||||||
?.toIntOrNull() else null,
|
|
||||||
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")
|
|
||||||
?.toIntOrNull() else null,
|
|
||||||
results = mutableListOf(),
|
results = mutableListOf(),
|
||||||
hasNextPage = false
|
hasNextPage = false
|
||||||
)
|
)
|
||||||
@@ -140,12 +127,8 @@ 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
|
||||||
}
|
}
|
||||||
@@ -154,7 +137,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?.isVisible = it.hasNextPage
|
progressAdapter.bar?.visibility = if (it.hasNextPage) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,10 +151,7 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
} else
|
} else
|
||||||
headerAdaptor.requestFocus?.run()
|
headerAdaptor.requestFocus?.run()
|
||||||
|
|
||||||
if (intent.getBooleanExtra("search", false)) {
|
if (intent.getBooleanExtra("search", false)) search()
|
||||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED)
|
|
||||||
search()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,9 +199,7 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
var state: Parcelable? = null
|
var state: Parcelable? = null
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
if (this::headerAdaptor.isInitialized) {
|
headerAdaptor.addHistory()
|
||||||
headerAdaptor.addHistory()
|
|
||||||
}
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
|
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package ani.dantotsu.media
|
package ani.dantotsu.media
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
@@ -13,7 +12,6 @@ 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.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -25,12 +23,9 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
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.STATE_CHECKED
|
import com.google.android.material.checkbox.MaterialCheckBox.*
|
||||||
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
|
||||||
@@ -46,20 +41,6 @@ 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)
|
||||||
@@ -108,78 +89,16 @@ 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, this).also {
|
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
||||||
activity.updateChips = { it.update() }
|
activity.updateChips = { it.update() }
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.searchChipRecycler.layoutManager =
|
binding.searchChipRecycler.layoutManager =
|
||||||
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||||
|
|
||||||
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 {
|
|
||||||
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
|
|
||||||
}
|
|
||||||
fun searchTitle() {
|
fun searchTitle() {
|
||||||
activity.result.apply {
|
activity.result.apply {
|
||||||
search =
|
search =
|
||||||
@@ -289,16 +208,13 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
binding.searchHistoryList.startAnimation(fadeInAnimation())
|
binding.searchHistoryList.startAnimation(fadeInAnimation())
|
||||||
binding.searchResultLayout.visibility = View.GONE
|
binding.searchResultLayout.visibility = View.GONE
|
||||||
binding.searchHistoryList.visibility = View.VISIBLE
|
binding.searchHistoryList.visibility = View.VISIBLE
|
||||||
binding.searchByImage.visibility = View.VISIBLE
|
|
||||||
} else {
|
} else {
|
||||||
if (binding.searchResultLayout.visibility != View.VISIBLE) {
|
if (binding.searchResultLayout.visibility != View.VISIBLE) {
|
||||||
binding.searchResultLayout.startAnimation(fadeInAnimation())
|
binding.searchResultLayout.startAnimation(fadeInAnimation())
|
||||||
binding.searchHistoryList.startAnimation(fadeOutAnimation())
|
binding.searchHistoryList.startAnimation(fadeOutAnimation())
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.searchResultLayout.visibility = View.VISIBLE
|
binding.searchResultLayout.visibility = View.VISIBLE
|
||||||
binding.searchHistoryList.visibility = View.GONE
|
binding.searchHistoryList.visibility = View.GONE
|
||||||
binding.searchByImage.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,10 +247,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchChipAdapter(
|
class SearchChipAdapter(val activity: SearchActivity) :
|
||||||
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()
|
||||||
|
|
||||||
@@ -351,12 +264,11 @@ 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.replace("_", " ")
|
text = chip.text
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
activity.result.removeChip(chip)
|
activity.result.removeChip(chip)
|
||||||
update()
|
update()
|
||||||
activity.search()
|
activity.search()
|
||||||
searchAdapter.updateFilterTextViewDrawable()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +277,6 @@ 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,15 +1,11 @@
|
|||||||
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
|
||||||
@@ -21,9 +17,6 @@ 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() {
|
||||||
@@ -45,54 +38,6 @@ 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?) {
|
||||||
|
|
||||||
@@ -102,157 +47,14 @@ 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 }
|
||||||
if (activity.result.type == "ANIME") {
|
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||||
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
|
||||||
@@ -265,23 +67,15 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
|||||||
binding.searchFilterCancel.setOnClickListener {
|
binding.searchFilterCancel.setOnClickListener {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
val format =
|
|
||||||
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
|
|
||||||
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
|
|
||||||
binding.searchStatus.setAdapter(
|
|
||||||
ArrayAdapter(
|
|
||||||
binding.root.context,
|
|
||||||
R.layout.item_dropdown,
|
|
||||||
format
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.searchSource.setText(activity.result.source?.replace("_", " "))
|
binding.searchSortBy.setText(activity.result.sort?.let {
|
||||||
binding.searchSource.setAdapter(
|
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
|
||||||
|
})
|
||||||
|
binding.searchSortBy.setAdapter(
|
||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
binding.root.context,
|
binding.root.context,
|
||||||
R.layout.item_dropdown,
|
R.layout.item_dropdown,
|
||||||
Anilist.source.toTypedArray()
|
resources.getStringArray(R.array.sort_by)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -290,25 +84,11 @@ 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.animeFormats else Anilist.mangaFormats).toTypedArray()
|
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (activity.result.type == "ANIME") {
|
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
|
||||||
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(
|
||||||
@@ -318,6 +98,16 @@ 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,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -21,6 +22,7 @@ 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) HAnimeSources else AnimeSources)[i!!]
|
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
|
||||||
} else {
|
} else {
|
||||||
anime = false
|
anime = false
|
||||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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
|
||||||
@@ -115,7 +114,7 @@ class StudioActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
binding.studioProgressBar.isGone = loaded
|
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||||
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(url: String): SubtitleType =
|
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
@@ -51,17 +51,21 @@ class SubtitleDownloader {
|
|||||||
downloadedType: DownloadedType
|
downloadedType: DownloadedType
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val directory = DownloadsManager.getSubDirectory(
|
val directory = DownloadsManager.getDirectory(
|
||||||
context,
|
context,
|
||||||
downloadedType.type,
|
downloadedType.type,
|
||||||
false,
|
|
||||||
downloadedType.title,
|
downloadedType.title,
|
||||||
downloadedType.chapter
|
downloadedType.chapter
|
||||||
) ?: throw Exception("Could not create directory")
|
)
|
||||||
val type = loadSubtitleType(url)
|
if (!directory.exists()) { //just in case
|
||||||
directory.findFile("subtitle.${type}")?.delete()
|
directory.mkdirs()
|
||||||
val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
|
}
|
||||||
?: throw Exception("Could not create subtitle file")
|
val type = loadSubtitleType(context, url)
|
||||||
|
val subtiteFile = File(directory, "subtitle.${type}")
|
||||||
|
if (subtiteFile.exists()) {
|
||||||
|
subtiteFile.delete()
|
||||||
|
}
|
||||||
|
subtiteFile.createNewFile()
|
||||||
|
|
||||||
val client = Injekt.get<NetworkHelper>().client
|
val client = Injekt.get<NetworkHelper>().client
|
||||||
val request = Request.Builder().url(url).build()
|
val request = Request.Builder().url(url).build()
|
||||||
@@ -73,8 +77,7 @@ class SubtitleDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reponse.body.byteStream().use { input ->
|
reponse.body.byteStream().use { input ->
|
||||||
subtitleFile.openOutputStream(context, false).use { output ->
|
subtiteFile.outputStream().use { output ->
|
||||||
if (output == null) throw Exception("Could not open output stream")
|
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
127
app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt
Normal file
127
app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.regex.Matcher
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class AnimeNameAdapter {
|
||||||
|
companion object {
|
||||||
|
const val episodeRegex =
|
||||||
|
"(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||||
|
const val failedEpisodeNumberRegex =
|
||||||
|
"(?<!part\\s)\\b(\\d+)\\b"
|
||||||
|
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||||
|
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
||||||
|
|
||||||
|
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
||||||
|
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||||
|
|
||||||
|
return if (subdubMatcher.find()) {
|
||||||
|
val soft = subdubMatcher.group(1)
|
||||||
|
val subdub = subdubMatcher.group(2)
|
||||||
|
val bed = subdubMatcher.group(3) ?: ""
|
||||||
|
|
||||||
|
val toggled = when (typeToSetTo) {
|
||||||
|
SubDubType.SUB -> "sub"
|
||||||
|
SubDubType.DUB -> "dub"
|
||||||
|
SubDubType.NULL -> ""
|
||||||
|
}
|
||||||
|
val toggledCasePreserved =
|
||||||
|
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
|
||||||
|
?.isUpperCase() == true
|
||||||
|
) toggled.replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) it.titlecase(
|
||||||
|
Locale.ROOT
|
||||||
|
) else it.toString()
|
||||||
|
} else toggled
|
||||||
|
|
||||||
|
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSubDub(text: String): SubDubType {
|
||||||
|
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||||
|
|
||||||
|
return if (subdubMatcher.find()) {
|
||||||
|
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
|
||||||
|
when (subdub) {
|
||||||
|
"sub" -> SubDubType.SUB
|
||||||
|
"dub" -> SubDubType.DUB
|
||||||
|
else -> SubDubType.NULL
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SubDubType.NULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SubDubType {
|
||||||
|
SUB, DUB, NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findSeasonNumber(text: String): Int? {
|
||||||
|
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||||
|
|
||||||
|
return if (seasonMatcher.find()) {
|
||||||
|
seasonMatcher.group(2)?.toInt()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findEpisodeNumber(text: String): Float? {
|
||||||
|
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||||
|
|
||||||
|
return if (episodeMatcher.find()) {
|
||||||
|
if (episodeMatcher.group(2) != null) {
|
||||||
|
episodeMatcher.group(2)?.toFloat()
|
||||||
|
} else {
|
||||||
|
val failedEpisodeNumberPattern: Pattern =
|
||||||
|
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||||
|
val failedEpisodeNumberMatcher: Matcher =
|
||||||
|
failedEpisodeNumberPattern.matcher(text)
|
||||||
|
if (failedEpisodeNumberMatcher.find()) {
|
||||||
|
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeEpisodeNumber(text: String): String {
|
||||||
|
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||||
|
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
val letterPattern = Regex("[a-zA-Z]")
|
||||||
|
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||||
|
removedNumber
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun removeEpisodeNumberCompletely(text: String): String {
|
||||||
|
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||||
|
val removedNumber = text.replace(regexPattern, "")
|
||||||
|
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||||
|
val failedEpisodeNumberPattern: Regex =
|
||||||
|
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
||||||
|
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||||
|
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removedNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -11,37 +12,26 @@ 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.FileUrl
|
import ani.dantotsu.*
|
||||||
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 ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||||
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
|
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -52,7 +42,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
|
||||||
|
|
||||||
@@ -64,25 +54,20 @@ 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
|
||||||
|
|
||||||
binding.faqbutton.setOnClickListener {
|
|
||||||
startActivity(
|
|
||||||
fragment.requireContext(),
|
|
||||||
Intent(fragment.requireContext(), FAQActivity::class.java),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
//Youtube
|
//Youtube
|
||||||
if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
|
if (media.anime!!.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
|
||||||
binding.animeSourceYT.visibility = View.VISIBLE
|
binding.animeSourceYT.visibility = View.VISIBLE
|
||||||
binding.animeSourceYT.setOnClickListener {
|
binding.animeSourceYT.setOnClickListener {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
|
||||||
fragment.requireContext().startActivity(intent)
|
fragment.requireContext().startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
|
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
|
||||||
binding.animeSourceDubbedText.text =
|
binding.animeSourceDubbedText.text =
|
||||||
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
||||||
@@ -106,12 +91,15 @@ class AnimeWatchAdapter(
|
|||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal(
|
||||||
|
PrefName.OfflineMode
|
||||||
|
)
|
||||||
|
) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
binding.animeSourceNameContainer.isGone = offline
|
binding.animeSourceNameContainer.visibility = offline
|
||||||
binding.animeSourceSettings.isGone = offline
|
binding.animeSourceSettings.visibility = offline
|
||||||
binding.animeSourceSearch.isGone = offline
|
binding.animeSourceSearch.visibility = offline
|
||||||
binding.animeSourceTitle.isGone = offline
|
binding.animeSourceTitle.visibility = offline
|
||||||
|
|
||||||
//Source Selection
|
//Source Selection
|
||||||
var source =
|
var source =
|
||||||
@@ -123,7 +111,8 @@ 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.isVisible = isDubAvailableSeparately()
|
binding.animeSourceDubbedCont.visibility =
|
||||||
|
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +131,8 @@ class AnimeWatchAdapter(
|
|||||||
changing = true
|
changing = true
|
||||||
binding.animeSourceDubbed.isChecked = selectDub
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
changing = false
|
changing = false
|
||||||
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
binding.animeSourceDubbedCont.visibility =
|
||||||
|
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||||
source = i
|
source = i
|
||||||
setLanguageList(0, i)
|
setLanguageList(0, i)
|
||||||
}
|
}
|
||||||
@@ -162,12 +152,14 @@ class AnimeWatchAdapter(
|
|||||||
changing = true
|
changing = true
|
||||||
binding.animeSourceDubbed.isChecked = selectDub
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
changing = false
|
changing = false
|
||||||
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
binding.animeSourceDubbedCont.visibility =
|
||||||
|
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
|
||||||
@@ -187,8 +179,7 @@ class AnimeWatchAdapter(
|
|||||||
R.drawable.ic_round_notifications_none_24,
|
R.drawable.ic_round_notifications_none_24,
|
||||||
R.color.bg_opp,
|
R.color.bg_opp,
|
||||||
R.color.violet_400,
|
R.color.violet_400,
|
||||||
fragment.subscribed,
|
fragment.subscribed
|
||||||
true
|
|
||||||
) {
|
) {
|
||||||
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
|
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
|
||||||
}
|
}
|
||||||
@@ -196,7 +187,7 @@ class AnimeWatchAdapter(
|
|||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
|
|
||||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||||
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
|
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
//Nested Button
|
//Nested Button
|
||||||
@@ -225,9 +216,9 @@ class AnimeWatchAdapter(
|
|||||||
else -> dialogBinding.animeSourceList
|
else -> dialogBinding.animeSourceList
|
||||||
}
|
}
|
||||||
when (style) {
|
when (style) {
|
||||||
0 -> dialogBinding.layoutText.setText(R.string.list)
|
0 -> dialogBinding.layoutText.text = "List"
|
||||||
1 -> dialogBinding.layoutText.setText(R.string.grid)
|
1 -> dialogBinding.layoutText.text = "Grid"
|
||||||
2 -> dialogBinding.layoutText.setText(R.string.compact)
|
2 -> dialogBinding.layoutText.text = "Compact"
|
||||||
else -> dialogBinding.animeSourceList
|
else -> dialogBinding.animeSourceList
|
||||||
}
|
}
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
@@ -239,24 +230,24 @@ class AnimeWatchAdapter(
|
|||||||
dialogBinding.animeSourceList.setOnClickListener {
|
dialogBinding.animeSourceList.setOnClickListener {
|
||||||
selected(it as ImageButton)
|
selected(it as ImageButton)
|
||||||
style = 0
|
style = 0
|
||||||
dialogBinding.layoutText.setText(R.string.list)
|
dialogBinding.layoutText.text = "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.setText(R.string.grid)
|
dialogBinding.layoutText.text = "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.setText(R.string.compact)
|
dialogBinding.layoutText.text = "Compact"
|
||||||
run = true
|
run = true
|
||||||
}
|
}
|
||||||
dialogBinding.animeWebviewContainer.setOnClickListener {
|
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||||
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||||
toast(R.string.webview_not_installed)
|
toast("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) {
|
||||||
@@ -309,6 +300,7 @@ 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) {
|
||||||
@@ -330,9 +322,7 @@ 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(),
|
||||||
@@ -366,6 +356,7 @@ 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) {
|
||||||
@@ -373,9 +364,9 @@ class AnimeWatchAdapter(
|
|||||||
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
||||||
|
|
||||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||||
val appEp = PrefManager.getCustomVal<String?>(
|
val appEp =
|
||||||
"${media.id}_current_ep", ""
|
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
|
||||||
)?.toIntOrNull() ?: 1
|
?: 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)) {
|
||||||
@@ -405,27 +396,21 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
val ep = media.anime.episodes!![continueEp]!!
|
val ep = media.anime.episodes!![continueEp]!!
|
||||||
|
|
||||||
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
|
val cleanedTitle = ep.title?.let { AnimeNameAdapter.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(
|
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
|
||||||
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 (
|
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
|
||||||
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
|
PrefName.WatchPercentage
|
||||||
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
|
)
|
||||||
) {
|
) {
|
||||||
binding.animeSourceContinue.performClick()
|
binding.animeSourceContinue.performClick()
|
||||||
fragment.continueEp = false
|
fragment.continueEp = false
|
||||||
@@ -436,35 +421,13 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.animeSourceProgressBar.visibility = View.GONE
|
binding.animeSourceProgressBar.visibility = View.GONE
|
||||||
|
if (media.anime.episodes!!.isNotEmpty())
|
||||||
val sourceFound = media.anime.episodes!!.isNotEmpty()
|
binding.animeSourceNotFound.visibility = View.GONE
|
||||||
binding.animeSourceNotFound.isGone = sourceFound
|
else
|
||||||
binding.faqbutton.isGone = sourceFound
|
binding.animeSourceNotFound.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
|
||||||
binding.faqbutton.visibility = View.GONE
|
|
||||||
clearChips()
|
clearChips()
|
||||||
binding.animeSourceProgressBar.visibility = View.VISIBLE
|
binding.animeSourceProgressBar.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
@@ -506,7 +469,8 @@ class AnimeWatchAdapter(
|
|||||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
displayTimer(media, binding.animeSourceContainer)
|
//Timer
|
||||||
|
countDown(media, binding.animeSourceContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,46 +17,39 @@ 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.FileUrl
|
import ani.dantotsu.*
|
||||||
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.dp
|
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||||
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.snackString
|
import ani.dantotsu.subcriptions.Notifications
|
||||||
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
|
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
||||||
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
|
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||||
|
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
import 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
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -199,16 +192,10 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val offline =
|
awaitAll(
|
||||||
!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
async { model.loadKitsuEpisodes(media) },
|
||||||
if (offline) {
|
async { model.loadFillerEpisodes(media) }
|
||||||
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
|
||||||
@@ -233,7 +220,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 (MediaNameAdapter.removeEpisodeNumberCompletely(
|
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
|
||||||
episode.title ?: ""
|
episode.title ?: ""
|
||||||
).isBlank()
|
).isBlank()
|
||||||
) media.anime!!.kitsuEpisodes!![i]?.title
|
) media.anime!!.kitsuEpisodes!![i]?.title
|
||||||
@@ -346,7 +333,16 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
var subscribed = false
|
var subscribed = false
|
||||||
fun onNotificationPressed(subscribed: Boolean, source: String) {
|
fun onNotificationPressed(subscribed: Boolean, source: String) {
|
||||||
this.subscribed = subscribed
|
this.subscribed = subscribed
|
||||||
saveSubscription(media, subscribed)
|
saveSubscription(requireContext(), media, subscribed)
|
||||||
|
if (!subscribed)
|
||||||
|
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
|
||||||
|
else
|
||||||
|
Notifications.createChannel(
|
||||||
|
requireContext(),
|
||||||
|
ANIME_GROUP,
|
||||||
|
getChannelId(true, media.id),
|
||||||
|
media.userPreferredName
|
||||||
|
)
|
||||||
snackString(
|
snackString(
|
||||||
if (subscribed) getString(R.string.subscribed_notification, source)
|
if (subscribed) getString(R.string.subscribed_notification, source)
|
||||||
else getString(R.string.unsubscribed_notification)
|
else getString(R.string.unsubscribed_notification)
|
||||||
@@ -357,12 +353,18 @@ 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) {
|
||||||
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).isVisible = show
|
val visibility = if (show) View.VISIBLE else View.GONE
|
||||||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).isVisible = show
|
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||||
activity.findViewById<CardView>(R.id.mediaCover).isVisible = show
|
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||||
activity.findViewById<CardView>(R.id.mediaClose).isVisible = show
|
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||||
activity.navBar.isVisible = show
|
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).isGone = show
|
try {
|
||||||
|
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||||
|
}
|
||||||
|
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||||
|
if (show) View.GONE else View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var itemSelected = false
|
var itemSelected = false
|
||||||
@@ -430,29 +432,7 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onAnimeEpisodeDownloadClick(i: String) {
|
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||||
activity?.let {
|
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||||
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) {
|
||||||
@@ -470,11 +450,10 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
i,
|
i,
|
||||||
MediaType.ANIME
|
DownloadedType.Type.ANIME
|
||||||
)
|
)
|
||||||
) {
|
)
|
||||||
episodeAdapter.purgeDownload(i)
|
episodeAdapter.purgeDownload(i)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@@ -483,13 +462,22 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
i,
|
i,
|
||||||
MediaType.ANIME
|
DownloadedType.Type.ANIME
|
||||||
)
|
)
|
||||||
) {
|
)
|
||||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||||
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
||||||
episodeAdapter.deleteDownload(i)
|
taskName,
|
||||||
}
|
""
|
||||||
|
) ?: ""
|
||||||
|
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() {
|
||||||
@@ -553,7 +541,7 @@ 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 (media.compareName(download.title)) {
|
if (download.title == media.mainName()) {
|
||||||
episodeAdapter.stopDownload(download.chapter)
|
episodeAdapter.stopDownload(download.chapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -573,8 +561,6 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
binding.mediaInfoProgressBar.visibility = progress
|
binding.mediaInfoProgressBar.visibility = progress
|
||||||
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||||
|
|
||||||
requireActivity().setNavigationTheme()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user