mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-17 08:33:56 +00:00
Compare commits
205 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
870cb751a4 | ||
|
|
4ffe9d7505 | ||
|
|
513b937e59 | ||
|
|
6113a10556 | ||
|
|
233f4bfb48 | ||
|
|
3fd01d582a | ||
|
|
00758af458 | ||
|
|
4477e3a0e1 | ||
|
|
e475cc5c01 | ||
|
|
3622d91886 | ||
|
|
3c46c21a25 | ||
|
|
44178b2de2 | ||
|
|
13f5d0978d | ||
|
|
70a50ece43 | ||
|
|
24147e746a | ||
|
|
386e02a564 | ||
|
|
865b96a219 | ||
|
|
dd38bb156b | ||
|
|
72c07b7d7a | ||
|
|
3f19cadffc | ||
|
|
670d16bd8e | ||
|
|
3d1040b280 | ||
|
|
cd3bb20afd | ||
|
|
91d1d2cf1d | ||
|
|
f8a6fad513 | ||
|
|
9d3d394c7d | ||
|
|
820a09b28f | ||
|
|
108285021e | ||
|
|
1d005585c8 | ||
|
|
714591dd2e | ||
|
|
6e399b32e1 | ||
|
|
4b413b78fe | ||
|
|
126bc6134e | ||
|
|
bf33f5d9c8 | ||
|
|
a8ff4fdc26 | ||
|
|
7ca44480a9 | ||
|
|
ea29449413 | ||
|
|
9ec448e503 | ||
|
|
70be4e92fb | ||
|
|
c0e3243ee6 | ||
|
|
b961701189 | ||
|
|
3619355cb4 | ||
|
|
674a512630 | ||
|
|
5e5277404e | ||
|
|
c242d9dd99 | ||
|
|
a5a94e5003 | ||
|
|
9b6dc1318d | ||
|
|
87535a9239 | ||
|
|
6be589618c | ||
|
|
a51e025c03 | ||
|
|
29e115ce41 | ||
|
|
f96d2ffaa5 | ||
|
|
47d05e737d | ||
|
|
3666758e6e | ||
|
|
e49f0dbf32 | ||
|
|
abe3f883ae | ||
|
|
e5cb7c7fdf | ||
|
|
79337b5e7f | ||
|
|
ae5907e6b3 | ||
|
|
04fb31eff9 | ||
|
|
9f7e01a1fb | ||
|
|
9ace8e5235 | ||
|
|
771cdcc163 | ||
|
|
58d5b5bc41 | ||
|
|
04538c52f2 | ||
|
|
dd994dcfab | ||
|
|
594b71dc16 | ||
|
|
cf7ccaebd1 | ||
|
|
8bde831794 | ||
|
|
2f30bdb6a8 | ||
|
|
4d28ae2e3e | ||
|
|
5fcbfeb3db | ||
|
|
f6c7b09d9b | ||
|
|
72c69e7c79 | ||
|
|
13a65c2bfa | ||
|
|
d2f118a86c | ||
|
|
ce11c71e95 | ||
|
|
e4574d6c03 | ||
|
|
d8c311fbd7 | ||
|
|
d6e6c6f8fb | ||
|
|
63c3058f5b | ||
|
|
0d5815d3c9 | ||
|
|
dec4996760 | ||
|
|
e0df092a70 | ||
|
|
da56aecd5e | ||
|
|
7688ffa39f | ||
|
|
d08e89bb63 | ||
|
|
5979479619 | ||
|
|
e1b968bfe0 | ||
|
|
36c476bc36 | ||
|
|
6bfadfa962 | ||
|
|
720b40afa7 | ||
|
|
75e90541c9 | ||
|
|
47b1940ace | ||
|
|
012b1cd79d | ||
|
|
ff3131d988 | ||
|
|
ba1725224a | ||
|
|
55bc2add85 | ||
|
|
9e96fd1e20 | ||
|
|
79d20b0b63 | ||
|
|
b2a44cfe09 | ||
|
|
146805af49 | ||
|
|
aabbe9198a | ||
|
|
a815bac15d | ||
|
|
86427a4c3c | ||
|
|
0d8a82568a | ||
|
|
95b2939532 | ||
|
|
76e11e5a3e | ||
|
|
2d5d02fd67 | ||
|
|
f30e6b7809 | ||
|
|
04f2034dd1 | ||
|
|
99b3bbaaad | ||
|
|
c0bccc027f | ||
|
|
51beac2d03 | ||
|
|
63a5150cea | ||
|
|
e34a20bce6 | ||
|
|
ca482ea9d4 | ||
|
|
e31d2ada4f | ||
|
|
c29147a681 | ||
|
|
92be9bf626 | ||
|
|
a02b8b7b0a | ||
|
|
1c1d14fff1 | ||
|
|
eff0a34c54 | ||
|
|
2dc3035a7c | ||
|
|
78f6ec27b3 | ||
|
|
6b868fa824 | ||
|
|
7951c2cf37 | ||
|
|
ea678ef55e | ||
|
|
fbbbf41595 | ||
|
|
f83d1d8d84 | ||
|
|
7bcc01b94e | ||
|
|
ff72f9dbdf | ||
|
|
b1210570d1 | ||
|
|
ef97b5679e | ||
|
|
6dfe0269bf | ||
|
|
77c57846ed | ||
|
|
19b5b11b07 | ||
|
|
27d4ce3c5b | ||
|
|
859aa01ec2 | ||
|
|
6d102f7be3 | ||
|
|
5ae1ead2c9 | ||
|
|
b1982013dc | ||
|
|
954fdde1c4 | ||
|
|
f177e2cf7c | ||
|
|
845ebb4868 | ||
|
|
b43171bb31 | ||
|
|
be07fad8f1 | ||
|
|
3375496ef2 | ||
|
|
df23b2f62f | ||
|
|
95cddbd409 | ||
|
|
d46f1b25eb | ||
|
|
378abe73c9 | ||
|
|
b5eda797b5 | ||
|
|
f704e322af | ||
|
|
dc21d28b83 | ||
|
|
eb17862177 | ||
|
|
fc023f307a | ||
|
|
ad1905c8fe | ||
|
|
85d54e8f5e | ||
|
|
ba09f7533c | ||
|
|
fa6e3a34b5 | ||
|
|
85ef4b3c12 | ||
|
|
89e18b0e2f | ||
|
|
1b50ffcf11 | ||
|
|
b3f83816c5 | ||
|
|
75b78886ae | ||
|
|
26d97da066 | ||
|
|
ab7bc15573 | ||
|
|
d43d643bbd | ||
|
|
3ca5efc177 | ||
|
|
04c858e6fd | ||
|
|
25046e4c11 | ||
|
|
5134776e2f | ||
|
|
cc29ebd75b | ||
|
|
2233f1ce44 | ||
|
|
a189802061 | ||
|
|
dca6ffdbbe | ||
|
|
859946a751 | ||
|
|
08bf1a2336 | ||
|
|
27743e3427 | ||
|
|
52b0cc4129 | ||
|
|
22abc2e21d | ||
|
|
fc8425b12a | ||
|
|
60fc1fa74b | ||
|
|
190e3ce7bb | ||
|
|
012024ab77 | ||
|
|
529bdd74c8 | ||
|
|
6e349b84c0 | ||
|
|
ab9b92035e | ||
|
|
37ec165319 | ||
|
|
958aa634b1 | ||
|
|
125a95285d | ||
|
|
bbaae2e776 | ||
|
|
f9090f59b7 | ||
|
|
1d740d33a0 | ||
|
|
633ec19c90 | ||
|
|
9b2015f4cf | ||
|
|
e65e7a79a5 | ||
|
|
0996639cac | ||
|
|
e5f58f20c7 | ||
|
|
d1e03b8237 | ||
|
|
917ffe644f | ||
|
|
02efc01a10 | ||
|
|
3016792f95 | ||
|
|
e1b50c86f3 |
25
.github/workflows/beta.yml
vendored
25
.github/workflows/beta.yml
vendored
@@ -48,9 +48,11 @@ jobs:
|
||||
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
|
||||
# Debugging: Print the variable to check its content
|
||||
echo "$COMMIT_LOGS"
|
||||
echo "$COMMIT_LOGS" > commit_log.txt
|
||||
shell: /usr/bin/bash -e {0}
|
||||
env:
|
||||
CI: true
|
||||
continue-on-error: true
|
||||
|
||||
- name: Save Current SHA for Next Run
|
||||
run: echo ${{ github.sha }} > last_sha.txt
|
||||
@@ -75,7 +77,7 @@ jobs:
|
||||
|
||||
- name: List files in the directory
|
||||
run: ls -l
|
||||
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
@@ -83,9 +85,11 @@ jobs:
|
||||
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Dantotsu
|
||||
retention-days: 5
|
||||
compression-level: 9
|
||||
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
||||
|
||||
- name: Upload APK to Discord and Telegram
|
||||
@@ -99,7 +103,7 @@ jobs:
|
||||
if [ ${#commit_messages} -gt $max_length ]; then
|
||||
commit_messages="${commit_messages:0:$max_length}... (truncated)"
|
||||
fi
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
#Telegram
|
||||
@@ -113,18 +117,13 @@ jobs:
|
||||
VERSION: ${{ env.VERSION }}
|
||||
|
||||
- name: Upload Current SHA as Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: last-sha
|
||||
path: last_sha.txt
|
||||
|
||||
|
||||
- name: Delete Old Pre-Releases
|
||||
id: delete-pre-releases
|
||||
uses: sgpublic/delete-release-action@master
|
||||
- name: Upload Commit log as Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
pre-release-drop: true
|
||||
pre-release-keep-count: 3
|
||||
pre-release-drop-tag: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: commit-log
|
||||
path: commit_log.txt
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,3 +31,6 @@ output.json
|
||||
|
||||
#other
|
||||
scripts/
|
||||
|
||||
#crowdin
|
||||
crowdin.yml
|
||||
@@ -6,6 +6,10 @@ plugins {
|
||||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
def gitCommitHash = providers.exec {
|
||||
commandLine("git", "rev-parse", "--verify", "--short", "HEAD")
|
||||
}.standardOutput.asText.get().trim()
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
|
||||
@@ -17,6 +21,7 @@ android {
|
||||
versionName "3.0.0"
|
||||
versionCode 300000000
|
||||
signingConfig signingConfigs.debug
|
||||
|
||||
}
|
||||
|
||||
flavorDimensions += "store"
|
||||
@@ -38,7 +43,7 @@ android {
|
||||
buildTypes {
|
||||
alpha {
|
||||
applicationIdSuffix ".beta" // keep as beta by popular request
|
||||
versionNameSuffix "-alpha01"
|
||||
versionNameSuffix "-alpha01-" + gitCommitHash
|
||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
|
||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
|
||||
debuggable System.getenv("CI") == null
|
||||
@@ -46,7 +51,7 @@ android {
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".beta"
|
||||
versionNameSuffix "-beta01"
|
||||
versionNameSuffix "-beta02"
|
||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
|
||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
|
||||
debuggable false
|
||||
@@ -75,11 +80,11 @@ android {
|
||||
|
||||
dependencies {
|
||||
|
||||
// FireBase
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.7.4')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
|
||||
// Core
|
||||
// FireBase
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.8.1')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.6.2'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.4'
|
||||
// Core
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.browser:browser:1.8.0'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
@@ -95,8 +100,9 @@ dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.10.0'
|
||||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
|
||||
// Glide
|
||||
// Glide
|
||||
ext.glide_version = '4.16.0'
|
||||
api "com.github.bumptech.glide:glide:$glide_version"
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
@@ -104,49 +110,48 @@ dependencies {
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.3.0'
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.3.1'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||
implementation "androidx.media3:media3-session:$exo_version"
|
||||
//media3 casting
|
||||
// Media3 Casting
|
||||
implementation "androidx.media3:media3-cast:$exo_version"
|
||||
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
||||
implementation "androidx.mediarouter:mediarouter:1.7.0"
|
||||
|
||||
// UI
|
||||
// UI
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
//implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||
implementation 'com.github.rebelonion:AnimatedBottomBar:v1.1.0'
|
||||
implementation 'com.github.RepoDevil:AnimatedBottomBar:7fcb9af'
|
||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc'
|
||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
|
||||
|
||||
// Markwon
|
||||
// Markwon
|
||||
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"
|
||||
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
|
||||
// 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
|
||||
// String Matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
|
||||
// Aniyomi
|
||||
// Aniyomi
|
||||
implementation 'io.reactivex:rxjava:1.3.8'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
|
||||
|
||||
@@ -5,12 +5,12 @@ import com.google.firebase.FirebaseApp
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.google.firebase.ktx.app
|
||||
|
||||
class FirebaseCrashlytics : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
FirebaseApp.initializeApp(context)
|
||||
}
|
||||
|
||||
override fun logException(e: Throwable) {
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
|
||||
@@ -85,13 +85,18 @@ object AppUpdater {
|
||||
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<GithubResponse>().assets?.find {
|
||||
it.browserDownloadURL.endsWith("apk")
|
||||
}?.browserDownloadURL.apply {
|
||||
if (this != null) activity.downloadUpdate(version, this)
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
val apks =
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(
|
||||
".apk"
|
||||
)
|
||||
}
|
||||
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) {
|
||||
logError(e)
|
||||
}
|
||||
@@ -111,24 +116,25 @@ object AppUpdater {
|
||||
}
|
||||
|
||||
private fun compareVersion(version: String): Boolean {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
"debug" -> BuildConfig.VERSION_NAME != version
|
||||
"alpha" -> false
|
||||
else -> {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
return BuildConfig.VERSION_NAME != version
|
||||
} 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("."))
|
||||
new > curr
|
||||
}
|
||||
|
||||
val new = toDouble(version.split("."))
|
||||
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
||||
return new > curr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="go.server.gojni" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
@@ -19,7 +21,7 @@
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||
@@ -38,6 +40,17 @@
|
||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
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>
|
||||
<package android:name="idm.internet.download.manager.plus" />
|
||||
<package android:name="idm.internet.download.manager" />
|
||||
@@ -49,6 +62,7 @@
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/ic_banner_foreground"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="${icon_placeholder}"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
@@ -57,9 +71,30 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Dantotsu"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup">
|
||||
tools:ignore="AllowBackup"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.CurrentlyAiringWidget"
|
||||
android:name=".widgets.upcoming.UpcomingWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/upcoming_widget_info" />
|
||||
</receiver>
|
||||
<activity
|
||||
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.statistics.ProfileStatsWidget"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -67,11 +102,10 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/currently_airing_widget_info" />
|
||||
android:resource="@xml/statistics_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
@@ -103,27 +137,61 @@
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.ExtensionsActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden"
|
||||
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
|
||||
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:windowSoftInputMode="adjustResize|stateHidden"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity
|
||||
android:name=".profile.FollowActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
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:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".profile.activity.NotificationActivity"
|
||||
android:label="Inbox Activity"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".others.imagesearch.ImageSearchActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
@@ -136,8 +204,9 @@
|
||||
android:name=".media.CalendarActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name=".media.user.ListActivity" />
|
||||
<activity android:name=".profile.SingleStatActivity"
|
||||
android:parentActivityName=".profile.ProfileActivity"/>
|
||||
<activity
|
||||
android:name=".profile.SingleStatActivity"
|
||||
android:parentActivityName=".profile.ProfileActivity" />
|
||||
<activity
|
||||
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||
android:excludeFromRecents="true"
|
||||
@@ -149,7 +218,7 @@
|
||||
android:name=".media.MediaDetailsActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@style/Theme.Dantotsu.NeverCutout"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden"/>
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity android:name=".media.CharacterDetailsActivity" />
|
||||
<activity android:name=".home.NoInternet" />
|
||||
<activity
|
||||
@@ -231,7 +300,6 @@
|
||||
<data android:host="discord.dantotsu.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".connections.anilist.UrlMedia"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
@@ -290,7 +358,9 @@
|
||||
</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" />
|
||||
@@ -299,30 +369,27 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
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:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<receiver android:name=".notifications.AlarmPermissionStateReceiver"
|
||||
<receiver
|
||||
android:name=".notifications.AlarmPermissionStateReceiver"
|
||||
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"
|
||||
<receiver
|
||||
android:name=".notifications.BootCompletedReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver"/>
|
||||
<receiver android:name=".notifications.comment.CommentNotificationReceiver"/>
|
||||
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver"/>
|
||||
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver" />
|
||||
<receiver android:name=".notifications.comment.CommentNotificationReceiver" />
|
||||
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver" />
|
||||
|
||||
<meta-data
|
||||
android:name="preloaded_fonts"
|
||||
@@ -340,25 +407,11 @@
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
||||
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name=".download.video.ExoplayerDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</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:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
@@ -381,6 +434,11 @@
|
||||
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".addons.torrent.ServerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.multidex.MultiDex
|
||||
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.PreferenceModule
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
@@ -41,6 +43,9 @@ class App : MultiDexApplication() {
|
||||
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||
private lateinit var novelExtensionManager: NovelExtensionManager
|
||||
private lateinit var torrentAddonManager: TorrentAddonManager
|
||||
private lateinit var downloadAddonManager: DownloadAddonManager
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
@@ -86,7 +91,7 @@ class App : MultiDexApplication() {
|
||||
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
||||
Logger.log("App: Logging started")
|
||||
|
||||
initializeNetwork(baseContext)
|
||||
initializeNetwork()
|
||||
|
||||
setupNotificationChannels()
|
||||
if (!LogcatLogger.isInstalled) {
|
||||
@@ -96,6 +101,8 @@ class App : MultiDexApplication() {
|
||||
animeExtensionManager = Injekt.get()
|
||||
mangaExtensionManager = Injekt.get()
|
||||
novelExtensionManager = Injekt.get()
|
||||
torrentAddonManager = Injekt.get()
|
||||
downloadAddonManager = Injekt.get()
|
||||
|
||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
@@ -115,13 +122,20 @@ class App : MultiDexApplication() {
|
||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
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)
|
||||
TaskScheduler.create(this, useAlarmManager).scheduleAllTasks(this)
|
||||
val scheduler = TaskScheduler.create(this, useAlarmManager)
|
||||
scheduler.scheduleAllTasks(this)
|
||||
scheduler.scheduleSingleWork(this)
|
||||
}
|
||||
|
||||
private fun setupNotificationChannels() {
|
||||
@@ -152,6 +166,10 @@ class App : MultiDexApplication() {
|
||||
|
||||
companion object {
|
||||
private var instance: App? = null
|
||||
|
||||
/** Reference to the application context.
|
||||
*
|
||||
* USE WITH EXTREME CAUTION!**/
|
||||
var context: Context? = null
|
||||
fun currentContext(): Context? {
|
||||
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
@@ -66,12 +67,9 @@ import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.ActivityCompat
|
||||
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.FileProvider
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
@@ -90,6 +88,7 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||
import ani.dantotsu.connections.anilist.Genre
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.bakaupdates.MangaUpdates
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||
import ani.dantotsu.media.Media
|
||||
@@ -100,6 +99,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||
import ani.dantotsu.util.CountUpTimer
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
@@ -132,9 +132,12 @@ import io.noties.markwon.html.TagHandlerNoOp
|
||||
import io.noties.markwon.image.AsyncDrawable
|
||||
import io.noties.markwon.image.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 uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -182,6 +185,11 @@ fun currActivity(): Activity? {
|
||||
var loadMedia: Int? = null
|
||||
var loadIsMAL = false
|
||||
|
||||
val Int.toPx
|
||||
get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
|
||||
).toInt()
|
||||
|
||||
fun initActivity(a: Activity) {
|
||||
val window = a.window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
@@ -201,13 +209,16 @@ fun initActivity(a: Activity) {
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||
?.apply {
|
||||
navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
|
||||
}
|
||||
}
|
||||
WindowInsetsControllerCompat(
|
||||
window,
|
||||
window.decorView
|
||||
).hide(WindowInsetsCompat.Type.statusBars())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
|
||||
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
) {
|
||||
window.decorView.rootWindowInsets?.displayCutout?.apply {
|
||||
if (boundingRects.size > 0) {
|
||||
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
|
||||
@@ -222,6 +233,7 @@ fun initActivity(a: Activity) {
|
||||
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||
navBarHeight =
|
||||
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
|
||||
}
|
||||
}
|
||||
if (a !is MainActivity) a.setNavigationTheme()
|
||||
@@ -262,6 +274,56 @@ fun Activity.setNavigationTheme() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
|
||||
*
|
||||
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
|
||||
*/
|
||||
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar) {
|
||||
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||
clipToPadding = false
|
||||
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
|
||||
*
|
||||
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
|
||||
*/
|
||||
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar, overlayView: View) {
|
||||
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||
overlayView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||
clipToPadding = false
|
||||
setPadding(
|
||||
paddingLeft,
|
||||
paddingTop,
|
||||
paddingRight,
|
||||
navBarHeight + navBar.measuredHeight + overlayView.measuredHeight
|
||||
)
|
||||
}
|
||||
|
||||
fun Activity.reloadActivity() {
|
||||
Refresh.all()
|
||||
finish()
|
||||
startActivity(Intent(this, this::class.java))
|
||||
initActivity(this)
|
||||
}
|
||||
|
||||
fun Activity.restartApp() {
|
||||
val mainIntent = Intent.makeRestartActivityTask(
|
||||
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
|
||||
)
|
||||
val component =
|
||||
ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
|
||||
try {
|
||||
startActivity(Intent().setComponent(component))
|
||||
} catch (e: Exception) {
|
||||
startActivity(mainIntent)
|
||||
}
|
||||
finishAndRemoveTask()
|
||||
PrefManager.setCustomVal("reload", true)
|
||||
}
|
||||
|
||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
@@ -359,7 +421,7 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
|
||||
dialog.setButton(
|
||||
DialogInterface.BUTTON_NEUTRAL,
|
||||
activity.getString(R.string.remove)
|
||||
) { dialog, which ->
|
||||
) { _, which ->
|
||||
if (which == DialogInterface.BUTTON_NEUTRAL) {
|
||||
date = FuzzyDate()
|
||||
}
|
||||
@@ -394,7 +456,6 @@ class InputFilterMinMax(
|
||||
return ""
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun isInRange(a: Double, b: Double, c: Double): Boolean {
|
||||
val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2]
|
||||
|
||||
@@ -407,7 +468,7 @@ class InputFilterMinMax(
|
||||
}
|
||||
|
||||
|
||||
class ZoomOutPageTransformer() :
|
||||
class ZoomOutPageTransformer :
|
||||
ViewPager2.PageTransformer {
|
||||
override fun transformPage(view: View, position: Float) {
|
||||
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
@@ -563,13 +624,35 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||
if (file?.url?.isNotEmpty() == true) {
|
||||
tryWith {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
if (file.url.startsWith("content://")) {
|
||||
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
|
||||
.override(size).into(this)
|
||||
} else {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ImageView.loadImage(file: FileUrl?, width: Int = 0, height: Int = 0) {
|
||||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||
if (file?.url?.isNotEmpty() == true) {
|
||||
tryWith {
|
||||
if (file.url.startsWith("content://")) {
|
||||
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
|
||||
.override(width, height).into(this)
|
||||
} else {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(width, height)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
||||
if (file?.exists() == true) {
|
||||
tryWith {
|
||||
@@ -712,6 +795,23 @@ fun openLinkInBrowser(link: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
fun openLinkInYouTube(link: String?) {
|
||||
link?.let {
|
||||
try {
|
||||
val videoIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
data = Uri.parse(link)
|
||||
setPackage("com.google.android.youtube")
|
||||
}
|
||||
currContext()!!.startActivity(videoIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
openLinkInBrowser(link)
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
@@ -803,31 +903,6 @@ fun savePrefs(
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
val requiredPermissions = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
||||
}.toTypedArray()
|
||||
|
||||
return if (requiredPermissions.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
requiredPermissions,
|
||||
DOWNLOADS_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
|
||||
|
||||
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
@@ -897,9 +972,10 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun countDown(media: Media, view: ViewGroup) {
|
||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
|
||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
|
||||
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()
|
||||
) {
|
||||
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
@@ -931,6 +1007,50 @@ fun countDown(media: Media, view: ViewGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
fun sinceWhen(media: Media, view: ViewGroup) {
|
||||
if (media.status != "RELEASING" && media.status != "HIATUS") return
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
|
||||
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
|
||||
val timeSince = (System.currentTimeMillis() -
|
||||
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
val v =
|
||||
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
|
||||
|
||||
object : CountUpTimer(86400000) {
|
||||
override fun onTick(second: Int) {
|
||||
val a = second + timeSince
|
||||
v.mediaCountdown.text = currActivity()?.getString(
|
||||
R.string.time_format,
|
||||
a / 86400,
|
||||
a % 86400 / 3600,
|
||||
a % 86400 % 3600 / 60,
|
||||
a % 86400 % 3600 % 60
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
// The legend will never die.
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun displayTimer(media: Media, view: ViewGroup) {
|
||||
when {
|
||||
media.anime != null -> countDown(media, view)
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
|
||||
else -> {} // No timer yet
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
|
||||
this.forEach {
|
||||
if (it.value.id == id) {
|
||||
@@ -1000,6 +1120,10 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
|
||||
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
|
||||
}
|
||||
|
||||
fun getAppString(res: Int): String {
|
||||
return currContext()?.getString(res) ?: ""
|
||||
}
|
||||
|
||||
fun toast(string: String?) {
|
||||
if (string != null) {
|
||||
Logger.log(string)
|
||||
@@ -1010,6 +1134,10 @@ fun toast(string: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
fun toast(res: Int) {
|
||||
toast(getAppString(res))
|
||||
}
|
||||
|
||||
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
|
||||
try { //I have no idea why this sometimes crashes for some people...
|
||||
if (s != null) {
|
||||
@@ -1050,6 +1178,10 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
|
||||
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>) :
|
||||
ArrayAdapter<T>(context, layoutId, items) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
@@ -1235,8 +1367,12 @@ fun blurImage(imageView: ImageView, banner: String?) {
|
||||
if (!(context as Activity).isDestroyed) {
|
||||
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
|
||||
Glide.with(context as Context)
|
||||
.load(GlideUrl(url))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.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)
|
||||
}
|
||||
@@ -1321,4 +1457,4 @@ fun buildMarkwon(
|
||||
}))
|
||||
.build()
|
||||
return markwon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
@@ -14,13 +13,11 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.OptIn
|
||||
@@ -36,14 +33,15 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import 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.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.DialogUserAgentBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
@@ -56,6 +54,7 @@ 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.asLiveBool
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
@@ -70,11 +69,13 @@ import com.google.android.material.textfield.TextInputEditText
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.Serializable
|
||||
@@ -87,6 +88,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private var load = false
|
||||
|
||||
|
||||
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -161,16 +163,16 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val backgroundDrawable = bottomNavBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
bottomNavBar.background = backgroundDrawable
|
||||
}
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
val offset = try {
|
||||
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
@@ -230,17 +232,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val preferences: SourcePreferences = Injekt.get()
|
||||
if (preferences.animeExtensionUpdatesCount()
|
||||
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
||||
) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"You have extension updates available!",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
binding.root.isMotionEventSplittingEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -284,6 +275,16 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
binding.root.doOnAttach {
|
||||
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) {
|
||||
when (fragment) {
|
||||
@@ -300,6 +301,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
var launched = false
|
||||
intent.extras?.let { extras ->
|
||||
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
|
||||
val mediaId = extras.getInt("mediaId", -1)
|
||||
@@ -312,6 +314,7 @@ class MainActivity : AppCompatActivity() {
|
||||
putExtra("mediaId", mediaId)
|
||||
putExtra("commentId", commentId)
|
||||
}
|
||||
launched = true
|
||||
startActivity(detailIntent)
|
||||
} else if (fragmentToLoad == "FEED" && activityId != -1) {
|
||||
val feedIntent = Intent(this, FeedActivity::class.java).apply {
|
||||
@@ -319,6 +322,7 @@ class MainActivity : AppCompatActivity() {
|
||||
putExtra("activityId", activityId)
|
||||
|
||||
}
|
||||
launched = true
|
||||
startActivity(feedIntent)
|
||||
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
||||
Logger.log("MainActivity, onCreate: $activityId")
|
||||
@@ -326,6 +330,7 @@ class MainActivity : AppCompatActivity() {
|
||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
||||
putExtra("activityId", activityId)
|
||||
}
|
||||
launched = true
|
||||
startActivity(notificationIntent)
|
||||
}
|
||||
}
|
||||
@@ -339,7 +344,7 @@ class MainActivity : AppCompatActivity() {
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) { it ->
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
@@ -364,7 +369,7 @@ class MainActivity : AppCompatActivity() {
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
if (mainViewPager.getCurrentItem() != selectedOption) {
|
||||
if (mainViewPager.currentItem != selectedOption) {
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
@@ -379,7 +384,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load) {
|
||||
if (!load && !launched) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMain(this@MainActivity)
|
||||
val id = intent.extras?.getInt("mediaId", 0)
|
||||
@@ -450,16 +455,26 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
|
||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||
val downloadCursor = index.getDownloads()
|
||||
while (downloadCursor.moveToNext()) {
|
||||
val download = downloadCursor.download
|
||||
if (download.state == Download.STATE_FAILED) {
|
||||
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||
|
||||
val torrentManager = Injekt.get<TorrentAddonManager>()
|
||||
fun startTorrent() {
|
||||
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() {
|
||||
@@ -467,34 +482,26 @@ class MainActivity : AppCompatActivity() {
|
||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
||||
}
|
||||
|
||||
private val Int.toPx get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
|
||||
).toInt()
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val params : ViewGroup.MarginLayoutParams =
|
||||
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
|
||||
val params: ViewGroup.MarginLayoutParams =
|
||||
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
params.updateMargins(bottom = 8.toPx)
|
||||
else
|
||||
params.updateMargins(bottom = 32.toPx)
|
||||
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 = "Enter your password to decrypt the file"
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
dialogView.userAgentTextBox.hint = "Password"
|
||||
dialogView.subtitle.visibility = View.VISIBLE
|
||||
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setView(dialogView)
|
||||
.setView(dialogView.root)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
password.fill('0')
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.others.webview.CloudFlare
|
||||
@@ -10,6 +9,7 @@ import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.nicehttp.addGenericDns
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -35,13 +35,13 @@ lateinit var defaultHeaders: Map<String, String>
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
lateinit var client: Requests
|
||||
|
||||
fun initializeNetwork(context: Context) {
|
||||
fun initializeNetwork() {
|
||||
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
||||
defaultHeaders = mapOf(
|
||||
"User-Agent" to
|
||||
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
||||
defaultUserAgentProvider()
|
||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||
)
|
||||
|
||||
|
||||
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
abstract class Addon {
|
||||
abstract val name: String
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Long
|
||||
|
||||
abstract class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
) : Addon()
|
||||
}
|
||||
128
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
128
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
@@ -0,0 +1,128 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.settings.InstallerSteps
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
||||
class AddonDownloader {
|
||||
companion object {
|
||||
private suspend fun check(repo: String): Pair<String, String> {
|
||||
return try {
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<AppUpdater.GithubResponse>(it)
|
||||
}
|
||||
val r = res.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
val md = r.body ?: ""
|
||||
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
|
||||
Logger.log("Git Version : $version")
|
||||
Pair(md, version)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error checking for update")
|
||||
Logger.log(e)
|
||||
Pair("", "")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun hasUpdate(repo: String, currentVersion: String): Boolean {
|
||||
val (_, version) = check(repo)
|
||||
return compareVersion(version, currentVersion)
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
activity: Activity,
|
||||
manager: AddonManager<*>,
|
||||
repo: String,
|
||||
currentVersion: String
|
||||
) {
|
||||
val (_, version) = check(repo)
|
||||
if (!compareVersion(version, currentVersion)) {
|
||||
toast(activity.getString(R.string.no_update_found))
|
||||
return
|
||||
}
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
val apks =
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<AppUpdater.GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(
|
||||
".apk"
|
||||
)
|
||||
}
|
||||
val apkToDownload =
|
||||
apks?.find { it.browserDownloadURL.contains(getCurrentABI()) }
|
||||
?: apks?.find { it.browserDownloadURL.contains("universal") }
|
||||
?: apks?.first()
|
||||
apkToDownload?.browserDownloadURL.apply {
|
||||
if (this != null) {
|
||||
val notificationManager =
|
||||
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val installerSteps = InstallerSteps(notificationManager, activity)
|
||||
manager.install(this)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||
{ error -> installerSteps.onError(error) {} },
|
||||
{ installerSteps.onComplete {} }
|
||||
)
|
||||
} else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ABI that the app is most likely running on.
|
||||
* @return The primary ABI for the device.
|
||||
*/
|
||||
private fun getCurrentABI(): String {
|
||||
return if (Build.SUPPORTED_ABIS.isNotEmpty()) {
|
||||
Build.SUPPORTED_ABIS[0]
|
||||
} else "Unknown"
|
||||
}
|
||||
|
||||
private fun compareVersion(newVersion: String, oldVersion: String): Boolean {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return try {
|
||||
list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
} catch (e: NumberFormatException) {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
val new = toDouble(newVersion.split("."))
|
||||
val curr = toDouble(oldVersion.split("."))
|
||||
return new > curr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
131
app/src/main/java/ani/dantotsu/addons/AddonInstallReceiver.kt
Normal file
131
app/src/main/java/ani/dantotsu/addons/AddonInstallReceiver.kt
Normal file
@@ -0,0 +1,131 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.filter
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.getPackageNameFromIntent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
|
||||
internal class AddonInstallReceiver : BroadcastReceiver() {
|
||||
private var listener: AddonListener? = null
|
||||
private var type: AddonType? = null
|
||||
|
||||
/**
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
fun setListener(listener: AddonListener, type: AddonType): AddonInstallReceiver {
|
||||
this.listener = listener
|
||||
this.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the events of the [filter] is received. When the package is an extension,
|
||||
* it's loaded in background and it notifies the [listener] when finished.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_PACKAGE_ADDED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
interface AddonListener {
|
||||
fun onAddonInstalled(result: LoadResult?)
|
||||
fun onAddonUpdated(result: LoadResult?)
|
||||
fun onAddonUninstalled(pkgName: String)
|
||||
|
||||
enum class ListenerAction {
|
||||
INSTALL, UPDATE, UNINSTALL
|
||||
}
|
||||
}
|
||||
143
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
143
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
@@ -0,0 +1,143 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import ani.dantotsu.addons.download.DownloadAddon
|
||||
import ani.dantotsu.addons.download.DownloadAddonApi
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.download.DownloadLoadResult
|
||||
import ani.dantotsu.addons.torrent.TorrentAddon
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonApi
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentLoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
|
||||
class AddonLoader {
|
||||
companion object {
|
||||
fun loadExtension(
|
||||
context: Context,
|
||||
packageName: String,
|
||||
className: String,
|
||||
type: AddonType
|
||||
): LoadResult? {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(ExtensionLoader.PACKAGE_FLAGS.toLong()))
|
||||
} else {
|
||||
pkgManager.getInstalledPackages(ExtensionLoader.PACKAGE_FLAGS)
|
||||
}
|
||||
|
||||
val extPkgs = installedPkgs.filter {
|
||||
isPackageAnExtension(
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
if (extPkgs.isEmpty()) return null
|
||||
if (extPkgs.size > 1) throw IllegalStateException("Multiple extensions with the same package name found")
|
||||
|
||||
val pkgName = extPkgs.first().packageName
|
||||
val pkgInfo = extPkgs.first()
|
||||
|
||||
val appInfo = try {
|
||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||
} catch (error: PackageManager.NameNotFoundException) {
|
||||
// Unlikely, but the package may have been uninstalled at this point
|
||||
Logger.log(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
val extName =
|
||||
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Dantotsu: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
Logger.log("Missing versionName for extension $extName")
|
||||
throw IllegalStateException("Missing versionName for extension $extName")
|
||||
}
|
||||
val classLoader =
|
||||
PathClassLoader(appInfo.sourceDir, appInfo.nativeLibraryDir, context.classLoader)
|
||||
val loadedClass = try {
|
||||
Class.forName(className, false, classLoader)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
}
|
||||
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> {
|
||||
val extension = instance as? TorrentAddonApi
|
||||
?: throw IllegalStateException("Extension is not a TorrentAddonApi")
|
||||
TorrentLoadResult.Success(
|
||||
TorrentAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
AddonType.DOWNLOAD -> {
|
||||
val extension = instance as? DownloadAddonApi
|
||||
?: throw IllegalStateException("Extension is not a DownloadAddonApi")
|
||||
DownloadLoadResult.Success(
|
||||
DownloadAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
TorrentAddonManager.TORRENT_CLASS,
|
||||
type
|
||||
)
|
||||
|
||||
AddonType.DOWNLOAD -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
DownloadAddonManager.DOWNLOAD_CLASS,
|
||||
type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
|
||||
return pkgInfo.packageName.equals(type)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
46
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
46
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import rx.Observable
|
||||
|
||||
abstract class AddonManager<T : Addon.Installed>(
|
||||
private val context: Context
|
||||
) {
|
||||
abstract var extension: T?
|
||||
abstract var name: String
|
||||
abstract var type: AddonType
|
||||
protected val installer by lazy { ExtensionInstaller(context) }
|
||||
var hasUpdate: Boolean = false
|
||||
protected set
|
||||
|
||||
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
|
||||
|
||||
abstract suspend fun init()
|
||||
abstract fun isAvailable(): Boolean
|
||||
abstract fun getVersion(): String?
|
||||
abstract fun getPackageName(): String?
|
||||
abstract fun hadError(context: Context): String?
|
||||
abstract fun updateInstallStep(id: Long, step: InstallStep)
|
||||
abstract fun setInstalling(id: Long)
|
||||
|
||||
fun uninstall() {
|
||||
getPackageName()?.let {
|
||||
installer.uninstallApk(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun addListenerAction(action: (AddonListener.ListenerAction) -> Unit) {
|
||||
onListenerAction = action
|
||||
}
|
||||
|
||||
fun removeListenerAction() {
|
||||
onListenerAction = null
|
||||
}
|
||||
|
||||
fun install(url: String): Observable<InstallStep> {
|
||||
return installer.downloadAndInstall(url, getPackageName() ?: "", name, type)
|
||||
}
|
||||
}
|
||||
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
abstract class LoadResult {
|
||||
|
||||
abstract class Success : LoadResult()
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class DownloadAddon : Addon() {
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: DownloadAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
interface DownloadAddonApi {
|
||||
|
||||
fun cancelDownload(sessionId: Long)
|
||||
|
||||
fun setDownloadPath(context: Context, uri: Uri): String
|
||||
|
||||
suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit)
|
||||
|
||||
suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long
|
||||
|
||||
fun getState(sessionId: Long): String
|
||||
|
||||
fun getStackTrace(sessionId: Long): String?
|
||||
|
||||
fun hadError(sessionId: Long): Boolean
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader
|
||||
import ani.dantotsu.addons.AddonInstallReceiver
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<DownloadAddon.Installed>(context) {
|
||||
|
||||
override var extension: DownloadAddon.Installed? = null
|
||||
override var name: String = "Download Addon"
|
||||
override var type = AddonType.DOWNLOAD
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
DOWNLOAD_PACKAGE,
|
||||
DOWNLOAD_CLASS,
|
||||
AddonType.DOWNLOAD
|
||||
) as? DownloadLoadResult
|
||||
result?.let {
|
||||
if (it is DownloadLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing Download extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (extension?.pkgName == pkgName) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val DOWNLOAD_PACKAGE = "dantotsu.downloadAddon"
|
||||
const val DOWNLOAD_CLASS = "ani.dantotsu.downloadAddon.DownloadAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Download-Addon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class DownloadLoadResult : LoadResult() {
|
||||
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class TorrentAddon : Addon() {
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: TorrentAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
|
||||
|
||||
interface TorrentAddonApi {
|
||||
|
||||
fun startServer(path: String)
|
||||
|
||||
fun stopServer()
|
||||
|
||||
fun echo(): String
|
||||
|
||||
fun removeTorrent(torrent: String)
|
||||
|
||||
fun addTorrent(
|
||||
link: String,
|
||||
title: String,
|
||||
poster: String,
|
||||
data: String,
|
||||
save: Boolean,
|
||||
): Torrent
|
||||
|
||||
fun getLink(torrent: Torrent, index: Int): String
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.addons.AddonInstallReceiver
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TorrentAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<TorrentAddon.Installed>(context) {
|
||||
override var extension: TorrentAddon.Installed? = null
|
||||
override var name: String = "Torrent Addon"
|
||||
override var type: AddonType = AddonType.TORRENT
|
||||
var torrentHash: String? = null
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
Logger.log("Torrent extension is not supported on this device.")
|
||||
error = context.getString(R.string.torrent_extension_not_supported)
|
||||
return
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
TORRENT_PACKAGE,
|
||||
TORRENT_CLASS,
|
||||
type
|
||||
) as TorrentLoadResult?
|
||||
result?.let {
|
||||
if (it is TorrentLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing torrent extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (pkgName == TORRENT_PACKAGE) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TORRENT_PACKAGE = "dantotsu.torrentAddon"
|
||||
const val TORRENT_CLASS = "ani.dantotsu.torrentAddon.TorrentAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Torrent-Addon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class TorrentLoadResult : LoadResult() {
|
||||
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
|
||||
}
|
||||
168
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
168
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
@@ -0,0 +1,168 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.ID_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
|
||||
class ServerService : Service() {
|
||||
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
|
||||
private val applicationContext = Injekt.get<Application>()
|
||||
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
intent?.let {
|
||||
if (it.action != null) {
|
||||
when (it.action) {
|
||||
ACTION_START -> {
|
||||
startServer()
|
||||
notification(applicationContext)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
ACTION_STOP -> {
|
||||
stopServer()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startServer() {
|
||||
serviceScope.launch {
|
||||
val echo = extension.echo()
|
||||
if (echo == "") {
|
||||
extension.startServer(filesDir.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopServer() {
|
||||
serviceScope.launch {
|
||||
extension.stopServer()
|
||||
applicationContext.cancelNotification(ID_TORRENT_SERVER)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notification(context: Context) {
|
||||
val exitPendingIntent =
|
||||
PendingIntent.getService(
|
||||
applicationContext,
|
||||
0,
|
||||
Intent(applicationContext, ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val builder = context.notificationBuilder(CHANNEL_TORRENT_SERVER) {
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setContentText("Torrent Server")
|
||||
setContentTitle("Server is running…")
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setUsesChronometer(true)
|
||||
addAction(
|
||||
R.drawable.ic_circle_cancel,
|
||||
"Stop",
|
||||
exitPendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
ID_TORRENT_SERVER,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
} else {
|
||||
startForeground(ID_TORRENT_SERVER, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_START = "start_torrent_server"
|
||||
const val ACTION_STOP = "stop_torrent_server"
|
||||
|
||||
fun isRunning(): Boolean {
|
||||
with(Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
|
||||
@Suppress("DEPRECATION") // We only need our services
|
||||
getRunningServices(Int.MAX_VALUE).forEach {
|
||||
if (ServerService::class.java.name.equals(it.service.className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun start() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_START
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun stop() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun wait(timeout: Int = -1): Boolean {
|
||||
var count = 0
|
||||
if (timeout < 0) {
|
||||
count = -20
|
||||
}
|
||||
var echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
while (echo == "") {
|
||||
Thread.sleep(1000)
|
||||
count++
|
||||
if (count > timeout) {
|
||||
return false
|
||||
}
|
||||
echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
}
|
||||
Logger.log("ServerService: Server started: $echo")
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
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.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
@@ -18,6 +20,7 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
@@ -29,6 +32,7 @@ import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
@kotlin.OptIn(ExperimentalSerializationApi::class)
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
@@ -36,10 +40,13 @@ class AppModule(val app: Application) : InjektModule {
|
||||
addSingletonFactory { DownloadsManager(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app) }
|
||||
addSingletonFactory { NetworkHelper(app).client }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
addSingletonFactory { MangaExtensionManager(app) }
|
||||
addSingletonFactory { NovelExtensionManager(app) }
|
||||
addSingletonFactory { TorrentAddonManager(app) }
|
||||
addSingletonFactory { DownloadAddonManager(app) }
|
||||
|
||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||
|
||||
@@ -3,17 +3,16 @@ package ani.dantotsu.connections.anilist
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import java.util.Calendar
|
||||
|
||||
@@ -40,20 +39,54 @@ object Anilist {
|
||||
"SCORE_DESC",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING_DESC",
|
||||
"START_DATE_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"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(
|
||||
"WINTER", "SPRING", "SUMMER", "FALL"
|
||||
)
|
||||
|
||||
val anime_formats = listOf(
|
||||
val animeFormats = listOf(
|
||||
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
||||
)
|
||||
|
||||
val manga_formats = listOf(
|
||||
val mangaFormats = listOf(
|
||||
"MANGA", "NOVEL", "ONE SHOT"
|
||||
)
|
||||
|
||||
@@ -117,6 +150,9 @@ object Anilist {
|
||||
episodesWatched = null
|
||||
chapterRead = null
|
||||
PrefManager.removeVal(PrefName.AnilistToken)
|
||||
//logout from comments api
|
||||
CommentsAPI.logout()
|
||||
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> executeQuery(
|
||||
@@ -163,7 +199,9 @@ object Anilist {
|
||||
toast("Rate limited. Try after $retry seconds")
|
||||
throw Exception("Rate limited after $retry seconds")
|
||||
}
|
||||
if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))}
|
||||
if (!json.text.startsWith("{")) {
|
||||
throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
}
|
||||
if (show) Logger.log("Anilist Response: ${json.text}")
|
||||
json.parsed()
|
||||
} else null
|
||||
|
||||
@@ -20,6 +20,7 @@ import ani.dantotsu.media.Character
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.Studio
|
||||
import ani.dantotsu.others.MalScraper
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
@@ -72,18 +73,19 @@ class AnilistQueries {
|
||||
media.cameFromContinue = false
|
||||
|
||||
val query =
|
||||
"""{Media(id:${media.id}){id mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
|
||||
"""{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
|
||||
runBlocking {
|
||||
val anilist = async {
|
||||
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
||||
if (response != null) {
|
||||
fun parse() {
|
||||
val fetchedMedia = response?.data?.media ?: return
|
||||
|
||||
val user = response?.data?.page
|
||||
media.source = fetchedMedia.source?.toString()
|
||||
media.countryOfOrigin = fetchedMedia.countryOfOrigin
|
||||
media.format = fetchedMedia.format?.toString()
|
||||
|
||||
media.favourites = fetchedMedia.favourites
|
||||
media.popularity = fetchedMedia.popularity
|
||||
media.startDate = fetchedMedia.startDate
|
||||
media.endDate = fetchedMedia.endDate
|
||||
|
||||
@@ -138,7 +140,15 @@ class AnilistQueries {
|
||||
?: "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>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -152,7 +162,7 @@ class AnilistQueries {
|
||||
Author(
|
||||
id = id,
|
||||
name = i.node?.name?.userPreferred,
|
||||
image = i.node?.image?.medium,
|
||||
image = i.node?.image?.large,
|
||||
role = when (i.role.toString()) {
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||
?: "MAIN"
|
||||
@@ -199,7 +209,24 @@ class AnilistQueries {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (user?.mediaList?.isNotEmpty() == true) {
|
||||
media.users = user.mediaList?.mapNotNull {
|
||||
it.user?.let { user ->
|
||||
if (user.id != Anilist.userid) {
|
||||
User(
|
||||
user.id,
|
||||
user.name ?: "Unknown",
|
||||
user.avatar?.large,
|
||||
"",
|
||||
it.status?.toString(),
|
||||
it.score,
|
||||
it.progress,
|
||||
fetchedMedia.episodes ?: fetchedMedia.chapters,
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}?.toCollection(arrayListOf()) ?: arrayListOf()
|
||||
}
|
||||
if (fetchedMedia.mediaListEntry != null) {
|
||||
fetchedMedia.mediaListEntry?.apply {
|
||||
media.userProgress = progress
|
||||
@@ -386,6 +413,7 @@ class AnilistQueries {
|
||||
returnArray.addAll(map.values)
|
||||
return returnArray
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
@@ -543,6 +571,7 @@ class AnilistQueries {
|
||||
returnMap["current$type"] = returnArray
|
||||
return
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
@@ -572,6 +601,7 @@ class AnilistQueries {
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
@@ -733,7 +763,7 @@ class AnilistQueries {
|
||||
}
|
||||
|
||||
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)
|
||||
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
||||
for (i in sorted.keys) {
|
||||
@@ -881,18 +911,23 @@ class AnilistQueries {
|
||||
sort: String? = null,
|
||||
genres: MutableList<String>? = null,
|
||||
tags: MutableList<String>? = null,
|
||||
status: String? = null,
|
||||
source: String? = null,
|
||||
format: String? = null,
|
||||
countryOfOrigin: String? = null,
|
||||
isAdult: Boolean = false,
|
||||
onList: Boolean? = null,
|
||||
excludedGenres: MutableList<String>? = null,
|
||||
excludedTags: MutableList<String>? = null,
|
||||
startYear: Int? = null,
|
||||
seasonYear: Int? = null,
|
||||
season: String? = null,
|
||||
id: Int? = null,
|
||||
hd: Boolean = false,
|
||||
adultOnly: Boolean = false
|
||||
): SearchResults? {
|
||||
val query = """
|
||||
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
|
||||
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
|
||||
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
|
||||
pageInfo {
|
||||
total
|
||||
@@ -937,14 +972,19 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
}
|
||||
""".replace("\n", " ").replace(""" """, "")
|
||||
val variables = """{"type":"$type","isAdult":$isAdult
|
||||
${if (adultOnly) ""","isAdult":true""" else ""}
|
||||
${if (onList != null) ""","onList":$onList""" else ""}
|
||||
${if (page != null) ""","page":"$page"""" else ""}
|
||||
${if (id != null) ""","id":"$id"""" else ""}
|
||||
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||
${if (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||
${if (type == "MANGA" && startYear != null) ""","yearGreater":${startYear}0000,"yearLesser":${startYear + 1}0000""" else ""}
|
||||
${if (season != null) ""","season":"$season"""" else ""}
|
||||
${if (search != null) ""","search":"$search"""" else ""}
|
||||
${if (source != null) ""","source":"$source"""" else ""}
|
||||
${if (sort != null) ""","sort":"$sort"""" else ""}
|
||||
${if (status != null) ""","status":"$status"""" else ""}
|
||||
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
||||
${if (countryOfOrigin != null) ""","countryOfOrigin":"$countryOfOrigin"""" else ""}
|
||||
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${
|
||||
if (excludedGenres?.isNotEmpty() == true)
|
||||
@@ -976,7 +1016,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
else ""
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
|
||||
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
|
||||
if (response?.media != null) {
|
||||
val responseArray = arrayListOf<Media>()
|
||||
@@ -1008,7 +1047,11 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
excludedGenres = excludedGenres,
|
||||
tags = tags,
|
||||
excludedTags = excludedTags,
|
||||
status = status,
|
||||
source = source,
|
||||
format = format,
|
||||
countryOfOrigin = countryOfOrigin,
|
||||
startYear = startYear,
|
||||
seasonYear = seasonYear,
|
||||
season = season,
|
||||
results = responseArray,
|
||||
@@ -1019,11 +1062,156 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
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(
|
||||
smaller: Boolean = true,
|
||||
greater: Long = 0,
|
||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||
): MutableList<Media>? {
|
||||
): MutableList<Media> {
|
||||
suspend fun execute(page: Int = 1): Page? {
|
||||
val query = """{
|
||||
Page(page:$page,perPage:50) {
|
||||
@@ -1070,41 +1258,26 @@ Page(page:$page,perPage:50) {
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||
}
|
||||
if (smaller) {
|
||||
val response = execute()?.airingSchedules ?: return null
|
||||
val idArr = mutableListOf<Int>()
|
||||
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||
return response.mapNotNull { i ->
|
||||
i.media?.let {
|
||||
if (!idArr.contains(it.id))
|
||||
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
} else null
|
||||
else null
|
||||
|
||||
var i = 1
|
||||
val list = mutableListOf<Media>()
|
||||
var res: Page? = null
|
||||
suspend fun next() {
|
||||
res = execute(i)
|
||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
||||
j.media?.let {
|
||||
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
|
||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
||||
} else null
|
||||
}
|
||||
}.toMutableList()
|
||||
} else {
|
||||
var i = 1
|
||||
val list = mutableListOf<Media>()
|
||||
var res: Page? = null
|
||||
suspend fun next() {
|
||||
res = execute(i)
|
||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
||||
j.media?.let {
|
||||
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
|
||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
||||
} else null
|
||||
}
|
||||
} ?: listOf())
|
||||
}
|
||||
next()
|
||||
while (res?.pageInfo?.hasNextPage == true) {
|
||||
next()
|
||||
i++
|
||||
}
|
||||
return list.reversed().toMutableList()
|
||||
} ?: listOf())
|
||||
}
|
||||
next()
|
||||
while (res?.pageInfo?.hasNextPage == true) {
|
||||
next()
|
||||
i++
|
||||
}
|
||||
return list.reversed().toMutableList()
|
||||
}
|
||||
|
||||
suspend fun getCharacterDetails(character: Character): Character {
|
||||
@@ -1290,19 +1463,39 @@ Page(page:$page,perPage:50) {
|
||||
}
|
||||
}
|
||||
}
|
||||
characters(page: $page,sort:FAVOURITES_DESC) {
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
}
|
||||
nodes{
|
||||
id
|
||||
name {
|
||||
first
|
||||
middle
|
||||
last
|
||||
full
|
||||
native
|
||||
userPreferred
|
||||
}
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
|
||||
var hasNextPage = true
|
||||
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
|
||||
var page = 0
|
||||
|
||||
val characters = arrayListOf<Character>()
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
hasNextPage = executeQuery<Query.Author>(
|
||||
query(page),
|
||||
force = true
|
||||
)?.data?.author?.staffMedia?.let {
|
||||
val query = executeQuery<Query.Author>(
|
||||
query(page), force = true
|
||||
)?.data?.author
|
||||
hasNextPage = query?.staffMedia?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
@@ -1317,6 +1510,20 @@ Page(page:$page,perPage:50) {
|
||||
}
|
||||
it.pageInfo?.hasNextPage == true
|
||||
} ?: 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")) {
|
||||
@@ -1324,6 +1531,7 @@ Page(page:$page,perPage:50) {
|
||||
yearMedia.remove("CANCELLED")
|
||||
yearMedia["CANCELLED"] = a
|
||||
}
|
||||
author.character = characters
|
||||
author.yearMedia = yearMedia
|
||||
return author
|
||||
}
|
||||
@@ -1390,17 +1598,16 @@ Page(page:$page,perPage:50) {
|
||||
"""{
|
||||
favoriteAnime:${userFavMediaQuery(true, 1, id)}
|
||||
favoriteManga:${userFavMediaQuery(false, 1, id)}
|
||||
animeMediaList:${bannerImageQuery("ANIME", id)}
|
||||
mangaMediaList:${bannerImageQuery("MANGA", id)}
|
||||
}""".trimIndent(), force = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun bannerImageQuery(type: String, id: Int?): String {
|
||||
return """MediaListCollection(userId: ${id}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } }"""
|
||||
}
|
||||
|
||||
suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? {
|
||||
suspend fun getNotifications(
|
||||
id: Int,
|
||||
page: Int = 1,
|
||||
resetNotification: Boolean = true
|
||||
): NotificationResponse? {
|
||||
val reset = if (resetNotification) "true" else "false"
|
||||
val 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,}}}}""",
|
||||
@@ -1415,7 +1622,12 @@ Page(page:$page,perPage:50) {
|
||||
return res
|
||||
}
|
||||
|
||||
suspend fun getFeed(userId: Int?, global: Boolean = false, page: Int = 1, activityId: Int? = null): FeedResponse? {
|
||||
suspend fun getFeed(
|
||||
userId: Int?,
|
||||
global: Boolean = false,
|
||||
page: Int = 1,
|
||||
activityId: Int? = null
|
||||
): FeedResponse? {
|
||||
val filter = if (activityId != null) "id:$activityId,"
|
||||
else if (userId != null) "userId:$userId,"
|
||||
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
|
||||
@@ -1426,14 +1638,44 @@ Page(page:$page,perPage:50) {
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun isUserFav(favType: AnilistMutations.FavType, id: Int): Boolean { //anilist isFavourite is broken, so we need to check it manually
|
||||
val res = getUserProfile(Anilist.userid?: return false)
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,45 +58,36 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
||||
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
||||
|
||||
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
||||
|
||||
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||
suspend fun setAnimePlanned() =
|
||||
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||
|
||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
||||
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
||||
|
||||
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
||||
|
||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||
suspend fun setMangaPlanned() =
|
||||
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||
|
||||
private val recommendation: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||
|
||||
suspend fun initHomePage() {
|
||||
val res = Anilist.query.initHomePage()
|
||||
@@ -112,8 +103,8 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
|
||||
suspend fun loadMain(context: FragmentActivity) {
|
||||
Anilist.getSavedToken()
|
||||
MAL.getSavedToken(context)
|
||||
Discord.getSavedToken(context)
|
||||
MAL.getSavedToken()
|
||||
Discord.getSavedToken()
|
||||
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||
}
|
||||
@@ -144,22 +135,19 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
sort = Anilist.sortBy[2],
|
||||
season = season,
|
||||
seasonYear = year,
|
||||
hd = true
|
||||
hd = true,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)?.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)
|
||||
|
||||
fun getPopular(): LiveData<SearchResults?> = animePopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
search_val: String? = null,
|
||||
searchVal: String? = null,
|
||||
genres: ArrayList<String>? = null,
|
||||
sort: String = Anilist.sortBy[1],
|
||||
onList: Boolean = true,
|
||||
@@ -167,10 +155,11 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
animePopular.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
search = search_val,
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
genres = genres,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -185,13 +174,43 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList
|
||||
r.onList,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
|
||||
)
|
||||
)
|
||||
|
||||
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() {
|
||||
@@ -209,29 +228,17 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
type,
|
||||
perPage = 10,
|
||||
sort = Anilist.sortBy[2],
|
||||
hd = true
|
||||
hd = true,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)?.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)
|
||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
search_val: String? = null,
|
||||
searchVal: String? = null,
|
||||
genres: ArrayList<String>? = null,
|
||||
sort: String = Anilist.sortBy[1],
|
||||
onList: Boolean = true,
|
||||
@@ -239,10 +246,11 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
mangaPopular.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
search = search_val,
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
genres = genres,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -257,17 +265,55 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
r.season,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
|
||||
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() {
|
||||
@@ -286,13 +332,17 @@ class AnilistSearch : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
r.season,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -305,11 +355,15 @@ class AnilistSearch : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
)
|
||||
@@ -347,11 +401,6 @@ class ProfileViewModel : ViewModel() {
|
||||
|
||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||
|
||||
private val listImages: MutableLiveData<ArrayList<String?>> =
|
||||
MutableLiveData<ArrayList<String?>>(arrayListOf())
|
||||
|
||||
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
||||
|
||||
suspend fun setData(id: Int) {
|
||||
val res = Anilist.query.initProfilePage(id)
|
||||
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
|
||||
@@ -367,30 +416,11 @@ class ProfileViewModel : ViewModel() {
|
||||
}
|
||||
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
|
||||
|
||||
val bannerImages = arrayListOf<String?>(null, null)
|
||||
val animeRandom = res?.data?.animeMediaList?.lists?.mapNotNull {
|
||||
it.entries?.mapNotNull { entry ->
|
||||
val imageUrl = entry.media?.bannerImage
|
||||
if (imageUrl != null && imageUrl != "null") imageUrl
|
||||
else null
|
||||
}
|
||||
}?.flatten()?.randomOrNull()
|
||||
bannerImages[0] = animeRandom
|
||||
val mangaRandom = res?.data?.mangaMediaList?.lists?.mapNotNull {
|
||||
it.entries?.mapNotNull { entry ->
|
||||
val imageUrl = entry.media?.bannerImage
|
||||
if (imageUrl != null && imageUrl != "null") imageUrl
|
||||
else null
|
||||
}
|
||||
}?.flatten()?.randomOrNull()
|
||||
bannerImages[1] = mangaRandom
|
||||
listImages.postValue(bannerImages)
|
||||
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
mangaFav.postValue(mangaFav.value)
|
||||
animeFav.postValue(animeFav.value)
|
||||
listImages.postValue(listImages.value)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.startMainActivity
|
||||
|
||||
@@ -11,13 +11,17 @@ data class SearchResults(
|
||||
var onList: Boolean? = null,
|
||||
var perPage: Int? = null,
|
||||
var search: String? = null,
|
||||
var countryOfOrigin: String? = null,
|
||||
var sort: String? = null,
|
||||
var genres: MutableList<String>? = null,
|
||||
var excludedGenres: MutableList<String>? = null,
|
||||
var tags: MutableList<String>? = null,
|
||||
var excludedTags: MutableList<String>? = null,
|
||||
var status: String? = null,
|
||||
var source: String? = null,
|
||||
var format: String? = null,
|
||||
var seasonYear: Int? = null,
|
||||
var startYear: Int? = null,
|
||||
var season: String? = null,
|
||||
var page: Int = 1,
|
||||
var results: MutableList<Media>,
|
||||
@@ -37,12 +41,24 @@ data class SearchResults(
|
||||
)
|
||||
)
|
||||
}
|
||||
status?.let {
|
||||
list.add(SearchChip("STATUS", currContext()!!.getString(R.string.filter_status, it)))
|
||||
}
|
||||
source?.let {
|
||||
list.add(SearchChip("SOURCE", currContext()!!.getString(R.string.filter_source, it)))
|
||||
}
|
||||
format?.let {
|
||||
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 {
|
||||
list.add(SearchChip("SEASON", it))
|
||||
}
|
||||
startYear?.let {
|
||||
list.add(SearchChip("START_YEAR", it.toString()))
|
||||
}
|
||||
seasonYear?.let {
|
||||
list.add(SearchChip("SEASON_YEAR", it.toString()))
|
||||
}
|
||||
@@ -74,8 +90,12 @@ data class SearchResults(
|
||||
fun removeChip(chip: SearchChip) {
|
||||
when (chip.type) {
|
||||
"SORT" -> sort = null
|
||||
"STATUS" -> status = null
|
||||
"SOURCE" -> source = null
|
||||
"FORMAT" -> format = null
|
||||
"COUNTRY" -> countryOfOrigin = null
|
||||
"SEASON" -> season = null
|
||||
"START_YEAR" -> startYear = null
|
||||
"SEASON_YEAR" -> seasonYear = null
|
||||
"GENRE" -> genres?.remove(chip.text)
|
||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||
|
||||
@@ -55,7 +55,7 @@ data class CharacterConnection(
|
||||
@SerialName("nodes") var nodes: List<Character>?,
|
||||
|
||||
// The pagination information
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
@SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -72,7 +72,7 @@ data class CharacterEdge(
|
||||
@SerialName("name") var name: String?,
|
||||
|
||||
// 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
|
||||
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,
|
||||
|
||||
@@ -24,7 +24,9 @@ class Query {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Media")
|
||||
val media: ani.dantotsu.connections.anilist.api.Media?
|
||||
val media: ani.dantotsu.connections.anilist.api.Media?,
|
||||
@SerialName("Page")
|
||||
val page: ani.dantotsu.connections.anilist.api.Page?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -147,9 +149,45 @@ class Query {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("animeMediaList") val animeMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("mangaMediaList") val mangaMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?
|
||||
@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?,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -283,13 +321,13 @@ class Query {
|
||||
val statistics: NNUserStatisticTypes,
|
||||
@SerialName("siteUrl")
|
||||
val siteUrl: String,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NNUserStatisticTypes(
|
||||
@SerialName("anime") var anime: NNUserStatistics,
|
||||
@SerialName("manga") var manga: NNUserStatistics
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NNUserStatistics(
|
||||
@@ -300,9 +338,9 @@ class Query {
|
||||
@SerialName("episodesWatched") var episodesWatched: Int,
|
||||
@SerialName("chaptersRead") var chaptersRead: Int,
|
||||
@SerialName("volumesRead") var volumesRead: Int,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
@Serializable
|
||||
data class UserFavourites(
|
||||
@SerialName("anime")
|
||||
val anime: UserMediaFavouritesCollection,
|
||||
@@ -314,13 +352,13 @@ class Query {
|
||||
val staff: UserStaffFavouritesCollection,
|
||||
@SerialName("studios")
|
||||
val studios: UserStudioFavouritesCollection,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserMediaFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserMediaImageFavorite>,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserMediaImageFavorite(
|
||||
@@ -328,13 +366,13 @@ class Query {
|
||||
val id: Int,
|
||||
@SerialName("coverImage")
|
||||
val coverImage: MediaCoverImage
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserCharacterFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserCharacterImageFavorite>,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserCharacterImageFavorite(
|
||||
@@ -346,19 +384,19 @@ class Query {
|
||||
val image: CharacterImage,
|
||||
@SerialName("isFavourite")
|
||||
val isFavourite: Boolean
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStaffFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStudioFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserStudioFavorite>,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStudioFavorite(
|
||||
@@ -366,7 +404,7 @@ class Query {
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
//----------------------------------------
|
||||
// Statistics
|
||||
@@ -375,12 +413,12 @@ class Query {
|
||||
data class StatisticsResponse(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
): java.io.Serializable {
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
val user: StatisticsUser?
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -21,6 +21,7 @@ enum class NotificationType(val value: String) {
|
||||
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
|
||||
MEDIA_MERGE("MEDIA_MERGE"),
|
||||
MEDIA_DELETION("MEDIA_DELETION"),
|
||||
|
||||
//custom
|
||||
COMMENT_REPLY("COMMENT_REPLY"),
|
||||
}
|
||||
@@ -84,9 +85,9 @@ data class Notification(
|
||||
@SerialName("createdAt")
|
||||
val createdAt: Int,
|
||||
@SerialName("media")
|
||||
val media: ani.dantotsu.connections.anilist.api.Media? = null,
|
||||
val media: Media? = null,
|
||||
@SerialName("user")
|
||||
val user: ani.dantotsu.connections.anilist.api.User? = null,
|
||||
val user: User? = null,
|
||||
@SerialName("message")
|
||||
val message: MessageActivity? = null,
|
||||
@SerialName("activity")
|
||||
|
||||
@@ -93,6 +93,7 @@ data class StaffConnection(
|
||||
// The pagination information
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StaffImage(
|
||||
// The character's image of media at its largest size
|
||||
@@ -101,6 +102,7 @@ data class StaffImage(
|
||||
// The character's image of media at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StaffEdge(
|
||||
var role: String?,
|
||||
|
||||
@@ -111,7 +111,7 @@ data class UserAvatar(
|
||||
|
||||
// The avatar of user at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStatisticTypes(
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package ani.dantotsu.connections.bakaupdates
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okio.ByteString.Companion.encode
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
class MangaUpdates {
|
||||
|
||||
private val Int?.dateFormat get() = String.format("%02d", this)
|
||||
|
||||
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
|
||||
|
||||
suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
|
||||
return tryWithSuspend {
|
||||
val query = JSONObject().apply {
|
||||
try {
|
||||
put("search", title.encode(Charset.forName("UTF-8")))
|
||||
startDate?.let {
|
||||
put(
|
||||
"start_date",
|
||||
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
|
||||
)
|
||||
}
|
||||
put("include_metadata", true)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
val res = client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
|
||||
coroutineScope {
|
||||
res.results?.map {
|
||||
async(Dispatchers.IO) {
|
||||
Logger.log(it.toString())
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
res.results?.first {
|
||||
it.metadata.series.lastUpdated?.timestamp != null
|
||||
&& (it.metadata.series.latestChapter != null
|
||||
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
|
||||
return results.metadata.series.latestChapter?.let {
|
||||
context.getString(R.string.chapter_number, it)
|
||||
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
|
||||
chapter.takeIf {
|
||||
it.toIntOrNull() == null
|
||||
} ?: context.getString(R.string.chapter_number, chapter.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MangaUpdatesResponse(
|
||||
@SerialName("total_hits")
|
||||
val totalHits: Int?,
|
||||
@SerialName("page")
|
||||
val page: Int?,
|
||||
@SerialName("per_page")
|
||||
val perPage: Int?,
|
||||
val results: List<Results>? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Results(
|
||||
val record: Record,
|
||||
val metadata: MetaData
|
||||
) {
|
||||
@Serializable
|
||||
data class Record(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("title")
|
||||
val title: String,
|
||||
@SerialName("volume")
|
||||
val volume: String?,
|
||||
@SerialName("chapter")
|
||||
val chapter: String?,
|
||||
@SerialName("release_date")
|
||||
val releaseDate: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MetaData(
|
||||
val series: Series
|
||||
) {
|
||||
@Serializable
|
||||
data class Series(
|
||||
@SerialName("series_id")
|
||||
val seriesId: Long?,
|
||||
@SerialName("title")
|
||||
val title: String?,
|
||||
@SerialName("latest_chapter")
|
||||
val latestChapter: Int?,
|
||||
@SerialName("last_updated")
|
||||
val lastUpdated: LastUpdated?
|
||||
) {
|
||||
@Serializable
|
||||
data class LastUpdated(
|
||||
@SerialName("timestamp")
|
||||
val timestamp: Long,
|
||||
@SerialName("as_rfc3339")
|
||||
val asRfc3339: String,
|
||||
@SerialName("as_string")
|
||||
val asString: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
object CommentsAPI {
|
||||
val address: String = "https://1224665.xyz:443"
|
||||
private const val ADDRESS: String = "https://1224665.xyz:443"
|
||||
var authToken: String? = null
|
||||
var userId: String? = null
|
||||
var isBanned: Boolean = false
|
||||
@@ -32,8 +32,13 @@ object CommentsAPI {
|
||||
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"
|
||||
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"
|
||||
@@ -61,7 +66,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
|
||||
val url = "$address/comments/parent/$id/$page"
|
||||
val url = "$ADDRESS/comments/parent/$id/$page"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
@@ -83,7 +88,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun getSingleComment(id: Int): Comment? {
|
||||
val url = "$address/comments/$id"
|
||||
val url = "$ADDRESS/comments/$id"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
@@ -105,7 +110,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun vote(commentId: Int, voteType: Int): Boolean {
|
||||
val url = "$address/comments/vote/$commentId/$voteType"
|
||||
val url = "$ADDRESS/comments/vote/$commentId/$voteType"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.post(url)
|
||||
@@ -121,7 +126,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
|
||||
val url = "$address/comments"
|
||||
val url = "$ADDRESS/comments"
|
||||
val body = FormBody.Builder()
|
||||
.add("user_id", userId ?: return null)
|
||||
.add("media_id", mediaId.toString())
|
||||
@@ -169,7 +174,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun deleteComment(commentId: Int): Boolean {
|
||||
val url = "$address/comments/$commentId"
|
||||
val url = "$ADDRESS/comments/$commentId"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.delete(url)
|
||||
@@ -185,7 +190,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun editComment(commentId: Int, content: String): Boolean {
|
||||
val url = "$address/comments/$commentId"
|
||||
val url = "$ADDRESS/comments/$commentId"
|
||||
val body = FormBody.Builder()
|
||||
.add("content", content)
|
||||
.build()
|
||||
@@ -204,7 +209,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun banUser(userId: String): Boolean {
|
||||
val url = "$address/ban/$userId"
|
||||
val url = "$ADDRESS/ban/$userId"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.post(url)
|
||||
@@ -225,7 +230,7 @@ object CommentsAPI {
|
||||
mediaTitle: String,
|
||||
reportedId: String
|
||||
): Boolean {
|
||||
val url = "$address/report/$commentId"
|
||||
val url = "$ADDRESS/report/$commentId"
|
||||
val body = FormBody.Builder()
|
||||
.add("username", username)
|
||||
.add("mediaName", mediaTitle)
|
||||
@@ -247,7 +252,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
|
||||
val url = "$address/notification/reply"
|
||||
val url = "$ADDRESS/notification/reply"
|
||||
val request = requestBuilder(client)
|
||||
val json = try {
|
||||
request.get(url)
|
||||
@@ -268,7 +273,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
|
||||
val url = "$address/user"
|
||||
val url = "$ADDRESS/user"
|
||||
val request = if (client != null) requestBuilder(client) else requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
@@ -310,7 +315,7 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
}
|
||||
val url = "$address/authenticate"
|
||||
val url = "$ADDRESS/authenticate"
|
||||
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
|
||||
repeat(MAX_RETRIES) {
|
||||
try {
|
||||
@@ -348,6 +353,17 @@ object CommentsAPI {
|
||||
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,
|
||||
@@ -388,7 +404,7 @@ object CommentsAPI {
|
||||
null
|
||||
}
|
||||
val message = parsed?.message ?: reason ?: error
|
||||
val fullMessage = if(code == 500) message else "$code: $message"
|
||||
val fullMessage = if (code == 500) message else "$code: $message"
|
||||
|
||||
toast(fullMessage)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class CrashlyticsStub : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun logException(e: Throwable) {
|
||||
Logger.log(e)
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ object Discord {
|
||||
var avatar: String? = null
|
||||
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
fun getSavedToken(): Boolean {
|
||||
token = PrefManager.getVal(
|
||||
PrefName.DiscordToken, null as String?
|
||||
)
|
||||
return token != null
|
||||
}
|
||||
|
||||
fun saveToken(context: Context, token: String) {
|
||||
fun saveToken(token: String) {
|
||||
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||
}
|
||||
|
||||
@@ -71,4 +71,6 @@ object Discord {
|
||||
const val application_Id = "1163925779692912771"
|
||||
const val small_Image: String =
|
||||
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
|
||||
const val small_Image_AniList: String =
|
||||
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
|
||||
}
|
||||
@@ -5,16 +5,12 @@ import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -37,7 +33,6 @@ import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.io.File
|
||||
import java.io.OutputStreamWriter
|
||||
|
||||
class DiscordService : Service() {
|
||||
private var heartbeat: Int = 0
|
||||
@@ -49,6 +44,7 @@ class DiscordService : Service() {
|
||||
private lateinit var heartbeatThread: Thread
|
||||
private lateinit var client: OkHttpClient
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
private val shouldLog = false
|
||||
var presenceStore = ""
|
||||
val json = Json {
|
||||
encodeDefaults = true
|
||||
@@ -67,7 +63,7 @@ class DiscordService : Service() {
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"discordRPC:backgroundPresence"
|
||||
)
|
||||
wakeLock.acquire()
|
||||
wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
|
||||
log("WakeLock Acquired")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val serviceChannel = NotificationChannel(
|
||||
@@ -162,8 +158,8 @@ class DiscordService : Service() {
|
||||
|
||||
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||
|
||||
var retryAttempts = 0
|
||||
val maxRetryAttempts = 10
|
||||
private var retryAttempts = 0
|
||||
private val maxRetryAttempts = 10
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
super.onOpen(webSocket, response)
|
||||
this@DiscordService.webSocket = webSocket
|
||||
@@ -232,7 +228,7 @@ class DiscordService : Service() {
|
||||
resume()
|
||||
resume = false
|
||||
} else {
|
||||
identify(webSocket, baseContext)
|
||||
identify(webSocket)
|
||||
log("WebSocket: Identified")
|
||||
}
|
||||
}
|
||||
@@ -245,13 +241,13 @@ class DiscordService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun identify(webSocket: WebSocket, context: Context) {
|
||||
private fun identify(webSocket: WebSocket) {
|
||||
val properties = JsonObject()
|
||||
properties.addProperty("os", "linux")
|
||||
properties.addProperty("browser", "unknown")
|
||||
properties.addProperty("device", "unknown")
|
||||
val d = JsonObject()
|
||||
d.addProperty("token", getToken(context))
|
||||
d.addProperty("token", getToken())
|
||||
d.addProperty("intents", 0)
|
||||
d.add("properties", properties)
|
||||
val payload = JsonObject()
|
||||
@@ -270,7 +266,7 @@ class DiscordService : Service() {
|
||||
retryAttempts++
|
||||
if (retryAttempts >= maxRetryAttempts) {
|
||||
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
|
||||
errorNotification("Could not set the presence", "Max Retry Attempts")
|
||||
errorNotification("Timeout setting presence", "Max Retry Attempts")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -311,7 +307,7 @@ class DiscordService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun getToken(context: Context): String {
|
||||
fun getToken(): String {
|
||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||
return if (token == null) {
|
||||
log("WebSocket: Token not found")
|
||||
@@ -349,13 +345,13 @@ class DiscordService : Service() {
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
//TODO: Request permission
|
||||
return
|
||||
}
|
||||
notificationManager.notify(2, builder.build())
|
||||
log("Error Notified")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun saveSimpleTestPresence() {
|
||||
val file = File(baseContext.cacheDir, "payload")
|
||||
//fill with test payload
|
||||
@@ -375,20 +371,22 @@ class DiscordService : Service() {
|
||||
log("WebSocket: Simple Test Presence Saved")
|
||||
}
|
||||
|
||||
fun setPresence(String: String) {
|
||||
fun setPresence(string: String) {
|
||||
log("WebSocket: Sending Presence payload")
|
||||
log(String)
|
||||
webSocket.send(String)
|
||||
log(string)
|
||||
webSocket.send(string)
|
||||
}
|
||||
|
||||
fun log(string: String) {
|
||||
//Logger.log(string)
|
||||
if (shouldLog) {
|
||||
Logger.log(string)
|
||||
}
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
log("Sending Resume payload")
|
||||
val d = JsonObject()
|
||||
d.addProperty("token", getToken(baseContext))
|
||||
d.addProperty("token", getToken())
|
||||
d.addProperty("session_id", sessionId)
|
||||
d.addProperty("seq", sequence)
|
||||
val json = JsonObject()
|
||||
@@ -404,7 +402,7 @@ class DiscordService : Service() {
|
||||
Thread.sleep(heartbeat.toLong())
|
||||
heartbeatSend(webSocket, sequence)
|
||||
log("WebSocket: Heartbeat Sent")
|
||||
} catch (e: InterruptedException) {
|
||||
} catch (ignored: InterruptedException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ class Login : AppCompatActivity() {
|
||||
}
|
||||
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
saveToken(this, token)
|
||||
saveToken(token)
|
||||
startMainActivity(this@Login)
|
||||
}
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
assets = Activity.Assets(
|
||||
largeImage = data.largeImage?.url?.discordUrl(),
|
||||
largeText = data.largeImage?.label,
|
||||
smallImage = data.smallImage?.url?.discordUrl(),
|
||||
smallText = data.smallImage?.label
|
||||
smallImage = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(),
|
||||
smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu",
|
||||
),
|
||||
buttons = data.buttons.map { it.label },
|
||||
metadata = Activity.Metadata(
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package ani.dantotsu.connections.github
|
||||
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.settings.Developer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
|
||||
class Contributors {
|
||||
|
||||
fun getContributors(): Array<Developer> {
|
||||
var developers = arrayOf<Developer>()
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val repo = getAppString(R.string.repo)
|
||||
val res = client.get("https://api.github.com/repos/$repo/contributors")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.forEach {
|
||||
if (it.login == "SunglassJerry") return@forEach
|
||||
val role = when (it.login) {
|
||||
"rebelonion" -> "Owner & Maintainer"
|
||||
"sneazy-ibo" -> "Contributor & Comment Moderator"
|
||||
"WaiWhat" -> "Icon Designer"
|
||||
else -> "Contributor"
|
||||
}
|
||||
developers = developers.plus(
|
||||
Developer(
|
||||
it.login,
|
||||
it.avatarUrl,
|
||||
role,
|
||||
it.htmlUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
developers = developers.plus(
|
||||
arrayOf(
|
||||
Developer(
|
||||
"MarshMeadow",
|
||||
"https://avatars.githubusercontent.com/u/88599122?v=4",
|
||||
"Beta Icon Designer & Website Maintainer",
|
||||
"https://github.com/MarshMeadow?tab=repositories"
|
||||
),
|
||||
Developer(
|
||||
"Zaxx69",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6342562-kxE8m4i7KUMK.png",
|
||||
"Telegram Admin",
|
||||
"https://anilist.co/user/6342562"
|
||||
),
|
||||
Developer(
|
||||
"Arif Alam",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6011177-2n994qtayiR9.jpg",
|
||||
"Discord & Comment Moderator",
|
||||
"https://anilist.co/user/6011177"
|
||||
),
|
||||
Developer(
|
||||
"SunglassJeery",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b5804776-FEKfP5wbz2xv.png",
|
||||
"Head Discord & Comment Moderator",
|
||||
"https://anilist.co/user/5804776"
|
||||
),
|
||||
Developer(
|
||||
"Excited",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6131921-toSoGWmKbRA1.png",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6131921"
|
||||
),
|
||||
Developer(
|
||||
"Gurjshan",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6363228-rWQ3Pl3WuxzL.png",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6363228"
|
||||
),
|
||||
Developer(
|
||||
"NekoMimi",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6244220-HOpImMGMQAxW.jpg",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6244220"
|
||||
),
|
||||
Developer(
|
||||
"Zaidsenior",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6049773"
|
||||
),
|
||||
Developer(
|
||||
"hastsu",
|
||||
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6183359"
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
return developers
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class GithubResponse(
|
||||
@SerialName("login")
|
||||
val login: String,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String,
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String
|
||||
)
|
||||
}
|
||||
54
app/src/main/java/ani/dantotsu/connections/github/Forks.kt
Normal file
54
app/src/main/java/ani/dantotsu/connections/github/Forks.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
package ani.dantotsu.connections.github
|
||||
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.settings.Developer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
|
||||
class Forks {
|
||||
|
||||
fun getForks(): Array<Developer> {
|
||||
var forks = arrayOf<Developer>()
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val res =
|
||||
client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks?sort=stargazers")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.forEach {
|
||||
forks = forks.plus(
|
||||
Developer(
|
||||
it.name,
|
||||
it.owner.avatarUrl,
|
||||
it.owner.login,
|
||||
it.htmlUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return forks
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class GithubResponse(
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
val owner: Owner,
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class Owner(
|
||||
@SerialName("login")
|
||||
val login: String,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
@@ -64,7 +63,7 @@ object MAL {
|
||||
}
|
||||
|
||||
|
||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
||||
suspend fun getSavedToken(): Boolean {
|
||||
return tryWithSuspend(false) {
|
||||
var res: ResponseToken =
|
||||
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||
@@ -77,7 +76,7 @@ object MAL {
|
||||
} ?: false
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
fun removeSavedToken() {
|
||||
token = null
|
||||
username = null
|
||||
userid = null
|
||||
|
||||
381
app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
Normal file
381
app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
Normal file
@@ -0,0 +1,381 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.anime.OfflineAnimeModel
|
||||
import ani.dantotsu.download.manga.OfflineMangaModel
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.Episode
|
||||
import ani.dantotsu.parsers.MangaChapter
|
||||
import ani.dantotsu.parsers.MangaImage
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
class DownloadCompat {
|
||||
companion object {
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun loadMediaCompat(downloadedType: DownloadedType): Media? {
|
||||
val type = when (downloadedType.type) {
|
||||
MediaType.MANGA -> "Manga"
|
||||
MediaType.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.titleName}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
return try {
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
|
||||
SAnimeImpl() // Provide an instance of SAnimeImpl
|
||||
})
|
||||
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
|
||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun loadOfflineAnimeModelCompat(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = when (downloadedType.type) {
|
||||
MediaType.MANGA -> "Manga"
|
||||
MediaType.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.titleName}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val mediaModel = loadMediaCompat(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalEpisode =
|
||||
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
|
||||
?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
|
||||
val chapters = " Chapters"
|
||||
val totalEpisodesList =
|
||||
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
|
||||
?: "~").toString()
|
||||
return OfflineAnimeModel(
|
||||
title,
|
||||
score,
|
||||
totalEpisode,
|
||||
totalEpisodesList,
|
||||
watchedEpisodes,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
isUserScored,
|
||||
coverUri,
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun loadOfflineMangaModelCompat(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = when (downloadedType.type) {
|
||||
MediaType.MANGA -> "Manga"
|
||||
MediaType.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.titleName}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val mediaModel = loadMediaCompat(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val chapters = " Chapters"
|
||||
return OfflineMangaModel(
|
||||
title,
|
||||
score,
|
||||
totalchapter,
|
||||
readchapter,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
isUserScored,
|
||||
coverUri,
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
suspend fun loadEpisodesCompat(
|
||||
animeLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sAnime: SAnime
|
||||
): List<Episode> {
|
||||
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${animeLocation}/$animeLink"
|
||||
)
|
||||
//get all of the folder names and add them to the list
|
||||
val episodes = mutableListOf<Episode>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
//put the title and episdode number in the extra data
|
||||
val extraData = mutableMapOf<String, String>()
|
||||
extraData["title"] = animeLink
|
||||
extraData["episode"] = it.name
|
||||
if (it.isDirectory) {
|
||||
val episode = Episode(
|
||||
it.name,
|
||||
"$animeLink - ${it.name}",
|
||||
it.name,
|
||||
null,
|
||||
null,
|
||||
extra = extraData,
|
||||
sEpisode = SEpisodeImpl()
|
||||
)
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
|
||||
return episodes
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
suspend fun loadChaptersCompat(
|
||||
mangaLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sManga: SManga
|
||||
): List<MangaChapter> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$mangaLink"
|
||||
)
|
||||
//get all of the folder names and add them to the list
|
||||
val chapters = mutableListOf<MangaChapter>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (it.isDirectory) {
|
||||
val chapter = MangaChapter(
|
||||
it.name,
|
||||
"$mangaLink/${it.name}",
|
||||
it.name,
|
||||
null,
|
||||
null,
|
||||
SChapter.create()
|
||||
)
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
|
||||
return chapters
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
suspend fun loadImagesCompat(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$chapterLink"
|
||||
)
|
||||
val images = mutableListOf<MangaImage>()
|
||||
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (it.isFile) {
|
||||
val image = MangaImage(it.absolutePath, false, null)
|
||||
images.add(image)
|
||||
}
|
||||
}
|
||||
images.sortBy { image ->
|
||||
val matchResult = imageNumberRegex.find(image.url.url)
|
||||
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
|
||||
}
|
||||
for (image in images) {
|
||||
Logger.log("imageNumber: ${image.url.url}")
|
||||
}
|
||||
return images
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun loadSubtitleCompat(title: String, episode: String): List<Subtitle>? {
|
||||
currContext()?.let {
|
||||
File(
|
||||
it.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title/$episode"
|
||||
).listFiles()?.forEach {
|
||||
if (it.name.contains("subtitle")) {
|
||||
return listOf(
|
||||
Subtitle(
|
||||
"Downloaded Subtitle",
|
||||
Uri.fromFile(it).toString(),
|
||||
determineSubtitletype(it.absolutePath)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun determineSubtitletype(url: String): SubtitleType {
|
||||
return when {
|
||||
url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
|
||||
url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun removeMediaCompat(context: Context, title: String, type: MediaType) {
|
||||
val subDirectory = if (type == MediaType.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == MediaType.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory/$title"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
directory.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun removeDownloadCompat(context: Context, downloadedType: DownloadedType) {
|
||||
val directory = if (downloadedType.type == MediaType.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||
)
|
||||
} else if (downloadedType.type == MediaType.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val animeLocation = "Dantotsu/Anime"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,27 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.removeDownloadCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.removeMediaCompat
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.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.reflect.TypeToken
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import java.io.Serializable
|
||||
|
||||
class DownloadsManager(private val context: Context) {
|
||||
@@ -15,11 +29,11 @@ class DownloadsManager(private val context: Context) {
|
||||
private val downloadsList = loadDownloads().toMutableList()
|
||||
|
||||
val mangaDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
||||
get() = downloadsList.filter { it.type == MediaType.MANGA }
|
||||
val animeDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
||||
get() = downloadsList.filter { it.type == MediaType.ANIME }
|
||||
val novelDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
||||
get() = downloadsList.filter { it.type == MediaType.NOVEL }
|
||||
|
||||
private fun saveDownloads() {
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
@@ -41,84 +55,72 @@ class DownloadsManager(private val context: Context) {
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(downloadedType: DownloadedType) {
|
||||
fun removeDownload(
|
||||
downloadedType: DownloadedType,
|
||||
toast: Boolean = true,
|
||||
onFinished: () -> Unit
|
||||
) {
|
||||
removeDownloadCompat(context, downloadedType)
|
||||
downloadsList.remove(downloadedType)
|
||||
removeDirectory(downloadedType)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
removeDirectory(downloadedType, toast)
|
||||
withContext(Dispatchers.Main) {
|
||||
onFinished()
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory/$title"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
fun removeMedia(title: String, type: MediaType) {
|
||||
removeMediaCompat(context, title, type)
|
||||
val baseDirectory = getBaseDirectory(context, type)
|
||||
val directory = baseDirectory?.findFolder(title)
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
snackString("Successfully deleted")
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
cleanDownloads()
|
||||
}
|
||||
when (type) {
|
||||
DownloadedType.Type.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
||||
MediaType.MANGA -> {
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA }
|
||||
}
|
||||
|
||||
DownloadedType.Type.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
||||
MediaType.ANIME -> {
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME }
|
||||
}
|
||||
|
||||
DownloadedType.Type.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
||||
MediaType.NOVEL -> {
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL }
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
private fun cleanDownloads() {
|
||||
cleanDownload(DownloadedType.Type.MANGA)
|
||||
cleanDownload(DownloadedType.Type.ANIME)
|
||||
cleanDownload(DownloadedType.Type.NOVEL)
|
||||
cleanDownload(MediaType.MANGA)
|
||||
cleanDownload(MediaType.ANIME)
|
||||
cleanDownload(MediaType.NOVEL)
|
||||
}
|
||||
|
||||
private fun cleanDownload(type: DownloadedType.Type) {
|
||||
private fun cleanDownload(type: MediaType) {
|
||||
// remove all folders that are not in the downloads list
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
val directory = getBaseDirectory(context, type)
|
||||
val downloadsSubLists = when (type) {
|
||||
MediaType.MANGA -> mangaDownloadedTypes
|
||||
MediaType.ANIME -> animeDownloadedTypes
|
||||
else -> novelDownloadedTypes
|
||||
}
|
||||
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()) {
|
||||
if (directory?.exists() == true && directory.isDirectory) {
|
||||
val files = directory.listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
val deleted = file.deleteRecursively()
|
||||
}
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.titleName == file.name }) {
|
||||
file.deleteRecursively(context, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,122 +128,129 @@ class DownloadsManager(private val context: Context) {
|
||||
val iterator = downloadsList.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val download = iterator.next()
|
||||
val downloadDir = File(directory, download.title)
|
||||
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
||||
val downloadDir = directory?.findFolder(download.titleName)
|
||||
if ((downloadDir?.exists() == false && download.type == type) || download.titleName.isBlank()) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||
{
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
val file = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/downloads.json"
|
||||
)
|
||||
if (file.parentFile?.exists() == false) {
|
||||
file.parentFile?.mkdirs()
|
||||
fun moveDownloadsDir(
|
||||
context: Context,
|
||||
oldUri: Uri,
|
||||
newUri: Uri,
|
||||
finished: (Boolean, String) -> Unit
|
||||
) {
|
||||
try {
|
||||
if (oldUri == newUri) {
|
||||
finished(false, "Source and destination are the same")
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
val oldBase =
|
||||
DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
|
||||
val newBase =
|
||||
DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
|
||||
val folder =
|
||||
oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
|
||||
folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object :
|
||||
FolderCallback() {
|
||||
override fun onFailed(errorCode: ErrorCode) {
|
||||
when (errorCode) {
|
||||
ErrorCode.CANCELED -> finished(false, "Move canceled")
|
||||
ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(
|
||||
false,
|
||||
"Cannot create file in target"
|
||||
)
|
||||
|
||||
ErrorCode.INVALID_TARGET_FOLDER -> finished(
|
||||
true,
|
||||
"Invalid target folder"
|
||||
) // seems to still work
|
||||
ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(
|
||||
false,
|
||||
"No space left on target path"
|
||||
)
|
||||
|
||||
ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
|
||||
ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(
|
||||
false,
|
||||
"Source folder not found"
|
||||
)
|
||||
|
||||
ErrorCode.STORAGE_PERMISSION_DENIED -> finished(
|
||||
false,
|
||||
"Storage permission denied"
|
||||
)
|
||||
|
||||
ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(
|
||||
false,
|
||||
"Target folder cannot have same path with source folder"
|
||||
)
|
||||
|
||||
else -> finished(false, "Failed to move downloads: $errorCode")
|
||||
}
|
||||
Logger.log("Failed to move downloads: $errorCode")
|
||||
super.onFailed(errorCode)
|
||||
}
|
||||
|
||||
override fun onCompleted(result: Result) {
|
||||
finished(true, "Successfully moved downloads")
|
||||
super.onCompleted(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
snackString("Error: ${e.message}")
|
||||
finished(false, "Failed to move downloads: ${e.message}")
|
||||
return
|
||||
}
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
|
||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||
return downloadsList.contains(downloadedType)
|
||||
}
|
||||
|
||||
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
||||
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
|
||||
return if (type == null) {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||
downloadsList.any { it.titleName == title && it.chapterName == chapter }
|
||||
} else {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
|
||||
downloadsList.any { it.titleName == title && it.chapterName == chapter && it.type == type }
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
|
||||
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
||||
val directory =
|
||||
baseDirectory?.findFolder(downloadedType.titleName)
|
||||
?.findFolder(downloadedType.chapterName)
|
||||
downloadsList.remove(downloadedType)
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
if (toast) snackString("Successfully deleted")
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
val destination = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val copied = directory.copyRecursively(destination, true)
|
||||
if (copied) {
|
||||
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: DownloadedType.Type) {
|
||||
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
fun purgeDownloads(type: MediaType) {
|
||||
val directory = getBaseDirectory(context, type)
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
snackString("Successfully deleted")
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
}
|
||||
|
||||
downloadsList.removeAll { it.type == type }
|
||||
@@ -249,62 +258,132 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val novelLocation = "Dantotsu/Novel"
|
||||
const val mangaLocation = "Dantotsu/Manga"
|
||||
const val animeLocation = "Dantotsu/Anime"
|
||||
private const val BASE_LOCATION = "Dantotsu"
|
||||
private const val MANGA_SUB_LOCATION = "Manga"
|
||||
private const val ANIME_SUB_LOCATION = "Anime"
|
||||
private const val NOVEL_SUB_LOCATION = "Novel"
|
||||
|
||||
fun getDirectory(
|
||||
context: Context,
|
||||
type: DownloadedType.Type,
|
||||
title: String,
|
||||
chapter: String? = null
|
||||
): File {
|
||||
return if (type == DownloadedType.Type.MANGA) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title"
|
||||
)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
} 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"
|
||||
)
|
||||
|
||||
MediaType.ANIME -> {
|
||||
base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
|
||||
}
|
||||
} else {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title"
|
||||
)
|
||||
|
||||
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,
|
||||
type: MediaType,
|
||||
overwrite: Boolean,
|
||||
title: String,
|
||||
chapter: String? = null
|
||||
): DocumentFile? {
|
||||
val baseDirectory = getBaseDirectory(context, type) ?: return null
|
||||
return if (chapter != null) {
|
||||
baseDirectory.findOrCreateFolder(title, false)
|
||||
?.findOrCreateFolder(chapter, overwrite)
|
||||
} else {
|
||||
baseDirectory.findOrCreateFolder(title, overwrite)
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME,
|
||||
NOVEL
|
||||
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 = "|\\?*<\":>+[]/'"
|
||||
fun String?.findValidName(): String {
|
||||
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||
}
|
||||
|
||||
data class DownloadedType(
|
||||
private val pTitle: String?,
|
||||
private val pChapter: String?,
|
||||
val type: MediaType,
|
||||
@Deprecated("use pTitle instead")
|
||||
private val title: String? = null,
|
||||
@Deprecated("use pChapter instead")
|
||||
private val chapter: String? = null
|
||||
) : Serializable {
|
||||
val titleName: String
|
||||
get() = title?:pTitle.findValidName()
|
||||
val chapterName: String
|
||||
get() = chapter?:pChapter.findValidName()
|
||||
}
|
||||
|
||||
@@ -9,32 +9,35 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
import 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.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
@@ -45,25 +48,21 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
|
||||
class AnimeDownloaderService : Service() {
|
||||
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
@@ -74,6 +73,7 @@ class AnimeDownloaderService : Service() {
|
||||
private val mutex = Mutex()
|
||||
private var isCurrentlyProcessing = false
|
||||
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
|
||||
private val ffExtension = Injekt.get<DownloadAddonManager>().extension?.extension
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
@@ -82,6 +82,11 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (ffExtension == null) {
|
||||
toast(getString(R.string.download_addon_not_found))
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder =
|
||||
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
@@ -89,6 +94,7 @@ class AnimeDownloaderService : Service() {
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(100, 0, false)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
@@ -157,27 +163,14 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
@UnstableApi
|
||||
fun cancelDownload(taskName: String) {
|
||||
val url =
|
||||
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
|
||||
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
||||
if (url.isEmpty()) {
|
||||
snackString("Failed to cancel download")
|
||||
return
|
||||
val sessionIds =
|
||||
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
|
||||
.map { it.sessionId }.toMutableList()
|
||||
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
||||
sessionIds.forEach {
|
||||
ffExtension!!.cancelDownload(it)
|
||||
}
|
||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
||||
DownloadService.sendSetStopReason(
|
||||
this@AnimeDownloaderService,
|
||||
ExoplayerDownloadService::class.java,
|
||||
url,
|
||||
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
|
||||
false
|
||||
)
|
||||
DownloadService.sendRemoveDownload(
|
||||
this@AnimeDownloaderService,
|
||||
ExoplayerDownloadService::class.java,
|
||||
url,
|
||||
false
|
||||
)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
mutex.withLock {
|
||||
downloadJobs[taskName]?.cancel()
|
||||
@@ -210,7 +203,6 @@ class AnimeDownloaderService : Service() {
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun download(task: AnimeDownloadTask) {
|
||||
try {
|
||||
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
||||
withContext(Dispatchers.Main) {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
@@ -221,18 +213,63 @@ class AnimeDownloaderService : Service() {
|
||||
true
|
||||
}
|
||||
|
||||
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
||||
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
currActivity()?.let {
|
||||
Helper.downloadVideo(
|
||||
it,
|
||||
task.video,
|
||||
task.subtitle
|
||||
)
|
||||
val outputDir = getSubDirectory(
|
||||
this@AnimeDownloaderService,
|
||||
MediaType.ANIME,
|
||||
false,
|
||||
task.title,
|
||||
task.episode
|
||||
) ?: throw Exception("Failed to create output directory")
|
||||
|
||||
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
|
||||
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
|
||||
?: throw Exception("Failed to create output file")
|
||||
|
||||
var percent = 0
|
||||
var totalLength = 0.0
|
||||
val path = ffExtension!!.setDownloadPath(
|
||||
this@AnimeDownloaderService,
|
||||
outputFile.uri
|
||||
)
|
||||
val headersStringBuilder = StringBuilder()
|
||||
task.video.file.headers.forEach {
|
||||
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
|
||||
}
|
||||
if (!task.video.file.headers.containsKey("User-Agent")) { //headers should never be empty now
|
||||
headersStringBuilder.append("\"").append("User-Agent: ")
|
||||
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
|
||||
}
|
||||
val probeRequest =
|
||||
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\""
|
||||
ffExtension.executeFFProbe(
|
||||
probeRequest
|
||||
) {
|
||||
if (it.toDoubleOrNull() != null) {
|
||||
totalLength = it.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
val headers = headersStringBuilder.toString()
|
||||
var request = "-headers $headers "
|
||||
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
|
||||
Logger.log("Request: $request")
|
||||
val ffTask =
|
||||
ffExtension.executeFFMpeg(request) {
|
||||
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||
val timeInMilliseconds = it
|
||||
if (timeInMilliseconds > 0 && totalLength > 0) {
|
||||
percent = ((it / 1000) / totalLength * 100).toInt()
|
||||
}
|
||||
Logger.log("Statistics: $it")
|
||||
}
|
||||
task.sessionId = ffTask
|
||||
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
|
||||
ffTask
|
||||
|
||||
saveMediaInfo(task)
|
||||
task.subtitle?.let {
|
||||
@@ -242,90 +279,120 @@ class AnimeDownloaderService : Service() {
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
}
|
||||
val downloadStarted =
|
||||
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
|
||||
|
||||
if (!downloadStarted) {
|
||||
Logger.log("Download failed to start")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed to start")
|
||||
broadcastDownloadFailed(task.episode)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
||||
// periodically check if the download is complete
|
||||
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed")
|
||||
Logger.log("Download failed: ${download.failureReason}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
||||
if (ffExtension.getState(ffTask) == "FAILED") {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
getTaskName(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
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.log("Download completed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download completed")
|
||||
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||
task.getTaskName(),
|
||||
task.video.file.url
|
||||
).apply()
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFinished(task.episode)
|
||||
break
|
||||
}
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
|
||||
Logger.log("Download stopped")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download stopped")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download stopped")
|
||||
break
|
||||
}
|
||||
broadcastDownloadProgress(
|
||||
task.episode,
|
||||
download.percentDownloaded.toInt()
|
||||
} Download failed"
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
toast("${getTaskName(task.title, task.episode)} Download failed")
|
||||
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
),
|
||||
false
|
||||
) {}
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${getTaskName(task.title, task.episode)}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
break
|
||||
}
|
||||
builder.setProgress(
|
||||
100, percent.coerceAtMost(99),
|
||||
false
|
||||
)
|
||||
broadcastDownloadProgress(
|
||||
task.episode,
|
||||
percent.coerceAtMost(99)
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
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) {
|
||||
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
||||
@@ -338,34 +405,24 @@ class AnimeDownloaderService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun hasDownloadStarted(
|
||||
downloadManager: DownloadManager,
|
||||
task: AnimeDownloadTask,
|
||||
timeout: Long
|
||||
): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < timeout) {
|
||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
return true
|
||||
}
|
||||
// Delay between each poll
|
||||
kotlinx.coroutines.delay(500)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||
launchIO {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.animeLocation}/${task.title}"
|
||||
)
|
||||
val episodeDirectory = File(directory, task.episode)
|
||||
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val directory =
|
||||
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val episodeDirectory =
|
||||
getSubDirectory(
|
||||
this@AnimeDownloaderService,
|
||||
MediaType.ANIME,
|
||||
false,
|
||||
task.title,
|
||||
task.episode
|
||||
)
|
||||
?: throw Exception("Directory not found")
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -399,14 +456,25 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
try {
|
||||
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@AnimeDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
@@ -417,13 +485,16 @@ class AnimeDownloaderService : Service() {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -490,14 +561,15 @@ class AnimeDownloaderService : Service() {
|
||||
val episodeImage: String? = null,
|
||||
val retries: Int = 2,
|
||||
val simultaneousDownloads: Int = 2,
|
||||
var sessionId: Long = -1
|
||||
) {
|
||||
fun getTaskName(): String {
|
||||
return "$title - $episode"
|
||||
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getTaskName(title: String, episode: String): String {
|
||||
return "$title - $episode"
|
||||
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,7 +583,6 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
object AnimeServiceDataSingleton {
|
||||
var video: Video? = null
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ani.dantotsu.download.anime
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -38,7 +37,6 @@ class OfflineAnimeAdapter(
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
|
||||
val view: View = convertView ?: when (style) {
|
||||
@@ -51,28 +49,27 @@ class OfflineAnimeAdapter(
|
||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
val totalEpisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
||||
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
||||
|
||||
if (style == 0) {
|
||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||
val episodes = view.findViewById<TextView>(R.id.itemTotal)
|
||||
episodes.text = " Episodes"
|
||||
bannerView.setImageURI(item.banner)
|
||||
totalepisodes.text = item.totalEpisodeList
|
||||
episodes.text = context.getString(R.string.episodes)
|
||||
bannerView.setImageURI(item.banner ?: item.image)
|
||||
totalEpisodes.text = item.totalEpisodeList
|
||||
} else if (style == 1) {
|
||||
val watchedEpisodes =
|
||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||
watchedEpisodes.text = item.watchedEpisode
|
||||
totalepisodes.text = " | " + item.totalEpisode
|
||||
totalEpisodes.text = context.getString(R.string.total_divider, item.totalEpisode)
|
||||
}
|
||||
|
||||
// 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
|
||||
typeView.visibility = View.VISIBLE
|
||||
imageView.setImageURI(item.image)
|
||||
|
||||
@@ -4,7 +4,6 @@ package ani.dantotsu.download.anime
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
@@ -22,26 +21,34 @@ import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.findValidName
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.openInputStream
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -53,9 +60,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
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.api.get
|
||||
import java.io.File
|
||||
|
||||
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
@@ -64,6 +75,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineAnimeAdapter
|
||||
private lateinit var total: TextView
|
||||
private var downloadsJob: Job = Job()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -110,10 +122,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
})
|
||||
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
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) {
|
||||
0 -> layoutList
|
||||
1 -> layoutcompact
|
||||
1 -> layoutCompact
|
||||
else -> layoutList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
@@ -134,7 +146,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
grid()
|
||||
}
|
||||
|
||||
layoutcompact.setOnClickListener {
|
||||
layoutCompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
@@ -154,11 +166,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
||||
getDownloads()
|
||||
gridView.adapter = adapter
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
@@ -166,20 +178,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val media =
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
media?.let {
|
||||
val mediaModel = getMedia(it)
|
||||
if (mediaModel == null) {
|
||||
snackString("Error loading media.json")
|
||||
return@let
|
||||
lifecycleScope.launch {
|
||||
val mediaModel = getMedia(it)
|
||||
if (mediaModel == null) {
|
||||
snackString("Error loading media.json")
|
||||
return@launch
|
||||
}
|
||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
}
|
||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
@@ -187,8 +201,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val type: DownloadedType.Type =
|
||||
DownloadedType.Type.ANIME
|
||||
val type: MediaType = MediaType.ANIME
|
||||
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
@@ -203,13 +216,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
if (mediaIds.isEmpty()) {
|
||||
snackString("No media found") // if this happens, terrible things have happened
|
||||
}
|
||||
for (mediaId in mediaIds) {
|
||||
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
|
||||
.removeDownload(mediaId.toString())
|
||||
}
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
@@ -237,7 +244,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
// Implement behavior for different scroll states if needed
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
@@ -250,7 +256,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
val visibility = first != null && first.top < 0
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||
scrollTop.isVisible = visibility
|
||||
}
|
||||
})
|
||||
initActivity(requireActivity())
|
||||
@@ -260,7 +266,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -280,29 +285,39 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
if (downloadsJob.isActive) {
|
||||
downloadsJob.cancel()
|
||||
}
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
}
|
||||
downloads = newAnimeDownloads
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.setItems(downloads)
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
downloads = newAnimeDownloads
|
||||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = when (downloadedType.type) {
|
||||
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 file from the directory and convert it to Media class
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return Media object
|
||||
*/
|
||||
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
return try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -314,8 +329,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return loadMediaCompat(downloadedType)
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
}
|
||||
?: return null
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
@@ -325,27 +345,28 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = when (downloadedType.type) {
|
||||
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 OfflineAnimeModel from the directory
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return OfflineAnimeModel object
|
||||
*/
|
||||
private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = downloadedType.type.asText()
|
||||
try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||
cover.uri
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
val banner = directory?.findFile("banner.jpg")
|
||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||
banner.uri
|
||||
} else null
|
||||
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
@@ -374,22 +395,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
return try {
|
||||
loadOfflineAnimeModelCompat(downloadedType)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,20 @@ import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||
@@ -30,6 +31,10 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
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.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
@@ -39,7 +44,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
@@ -51,8 +55,6 @@ import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
@@ -189,13 +191,20 @@ class MangaDownloaderService : Service() {
|
||||
true
|
||||
}
|
||||
|
||||
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
getSubDirectory(
|
||||
this@MangaDownloaderService,
|
||||
MediaType.MANGA,
|
||||
false,
|
||||
task.title,
|
||||
task.chapter
|
||||
)?.deleteRecursively(this@MangaDownloaderService)
|
||||
|
||||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
@@ -211,8 +220,7 @@ class MangaDownloaderService : Service() {
|
||||
while (bitmap == null && retryCount < task.retries) {
|
||||
bitmap = image.fetchAndProcessImage(
|
||||
image.page,
|
||||
image.source,
|
||||
this@MangaDownloaderService
|
||||
image.source
|
||||
)
|
||||
retryCount++
|
||||
}
|
||||
@@ -246,7 +254,7 @@ class MangaDownloaderService : Service() {
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
DownloadedType.Type.MANGA
|
||||
MediaType.MANGA
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.chapter)
|
||||
@@ -264,24 +272,18 @@ class MangaDownloaderService : Service() {
|
||||
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||
try {
|
||||
// Define the directory within the private external storage space
|
||||
val directory = File(
|
||||
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$title/$chapter"
|
||||
)
|
||||
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Create a file reference within that directory for your image
|
||||
val file = File(directory, fileName)
|
||||
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile(fileName)?.forceDelete(this)
|
||||
// Create a file reference within that directory for the image
|
||||
val file =
|
||||
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
|
||||
|
||||
// Use a FileOutputStream to write the bitmap to the file
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
file.openOutputStream(this, false).use { outputStream ->
|
||||
if (outputStream == null) throw Exception("Output stream is null")
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Exception while saving image: ${e.message}")
|
||||
snackString("Exception while saving image: ${e.message}")
|
||||
@@ -292,13 +294,12 @@ class MangaDownloaderService : Service() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
launchIO {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${task.title}"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val directory =
|
||||
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -313,7 +314,10 @@ class MangaDownloaderService : Service() {
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
file.writeText(jsonString)
|
||||
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
@@ -328,7 +332,7 @@ class MangaDownloaderService : Service() {
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
@@ -338,14 +342,16 @@ class MangaDownloaderService : Service() {
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -37,7 +36,6 @@ class OfflineMangaAdapter(
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
|
||||
val view: View = convertView ?: when (style) {
|
||||
@@ -50,7 +48,6 @@ class OfflineMangaAdapter(
|
||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
@@ -60,14 +57,14 @@ class OfflineMangaAdapter(
|
||||
if (style == 0) {
|
||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||
val chapters = view.findViewById<TextView>(R.id.itemTotal)
|
||||
chapters.text = " Chapters"
|
||||
bannerView.setImageURI(item.banner)
|
||||
chapters.text = context.getString(R.string.chapters)
|
||||
bannerView.setImageURI(item.banner ?: item.image)
|
||||
totalChapter.text = item.totalChapter
|
||||
} else if (style == 1) {
|
||||
val readChapter =
|
||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||
readChapter.text = item.readChapter
|
||||
totalChapter.text = " | " + item.totalChapter
|
||||
totalChapter.text = context.getString(R.string.total_divider, item.totalChapter)
|
||||
}
|
||||
|
||||
// Bind item data to the views
|
||||
|
||||
@@ -3,7 +3,6 @@ package ani.dantotsu.download.manga
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
@@ -20,25 +19,34 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.findValidName
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.openInputStream
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -46,9 +54,13 @@ import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import 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.api.get
|
||||
import java.io.File
|
||||
|
||||
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
@@ -57,6 +69,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineMangaAdapter
|
||||
private lateinit var total: TextView
|
||||
private var downloadsJob: Job = Job()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -146,11 +159,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||
getDownloads()
|
||||
gridView.adapter = adapter
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text =
|
||||
@@ -159,17 +172,18 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val media =
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
media?.let {
|
||||
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
lifecycleScope.launch {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
@@ -178,11 +192,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: DownloadedType.Type =
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||
DownloadedType.Type.MANGA
|
||||
val type: MediaType =
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) {
|
||||
MediaType.MANGA
|
||||
} else {
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
}
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
@@ -192,9 +206,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text =
|
||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
@@ -223,7 +234,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
// Implement behavior for different scroll states if needed
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
@@ -234,7 +244,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
) {
|
||||
val first = view.getChildAt(0)
|
||||
val visibility = first != null && first.top < 0
|
||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||
scrollTop.isVisible = visibility
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
}
|
||||
@@ -246,7 +256,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -266,46 +275,62 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
if (downloadsJob.isActive) {
|
||||
downloadsJob.cancel()
|
||||
}
|
||||
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 = listOf()
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
}
|
||||
downloads += newNovelDownloads
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.setItems(downloads)
|
||||
total.text =
|
||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
downloads += newNovelDownloads
|
||||
|
||||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = when (downloadedType.type) {
|
||||
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 file from the directory and convert it to Media class
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return Media object
|
||||
*/
|
||||
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
return try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return DownloadCompat.loadMediaCompat(downloadedType)
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
}
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
@@ -315,41 +340,38 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
DownloadedType.Type.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = downloadedType.type.asText()
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||
cover.uri
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
val banner = directory?.findFile("banner.jpg")
|
||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||
banner.uri
|
||||
} else null
|
||||
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val readChapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val chapters = " Chapters"
|
||||
return OfflineMangaModel(
|
||||
title,
|
||||
score,
|
||||
totalchapter,
|
||||
readchapter,
|
||||
totalChapter,
|
||||
readChapter,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
@@ -358,21 +380,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
return try {
|
||||
loadOfflineMangaModelCompat(downloadedType)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,21 +9,25 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
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.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
@@ -33,7 +37,6 @@ import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -46,8 +49,6 @@ import okio.sink
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
@@ -64,7 +65,7 @@ class NovelDownloaderService : Service() {
|
||||
private val mutex = Mutex()
|
||||
private var isCurrentlyProcessing = false
|
||||
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
private val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
@@ -247,27 +248,30 @@ class NovelDownloaderService : Service() {
|
||||
|
||||
networkHelper.downloadClient.newCall(request).execute().use { response ->
|
||||
// Ensure the response is successful and has a body
|
||||
if (!response.isSuccessful || response.body == null) {
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Failed to download file: ${response.message}")
|
||||
}
|
||||
val directory = getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
task.title,
|
||||
task.chapter
|
||||
) ?: throw Exception("Directory not found")
|
||||
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
|
||||
|
||||
val file = File(
|
||||
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()
|
||||
val file = directory.createFile("application/epub+zip", "0.epub")
|
||||
?: throw Exception("File not created")
|
||||
|
||||
//download cover
|
||||
task.coverUrl?.let {
|
||||
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
||||
}
|
||||
val outputStream =
|
||||
this@NovelDownloaderService.contentResolver.openOutputStream(file.uri)
|
||||
?: throw Exception("Could not open OutputStream")
|
||||
|
||||
val sink = file.sink().buffer()
|
||||
val sink = outputStream.sink().buffer()
|
||||
val responseBody = response.body
|
||||
val totalBytes = responseBody.contentLength()
|
||||
var downloadedBytes = 0L
|
||||
@@ -335,7 +339,7 @@ class NovelDownloaderService : Service() {
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
@@ -352,13 +356,16 @@ class NovelDownloaderService : Service() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
launchIO {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${task.title}"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val directory =
|
||||
getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
task.title
|
||||
) ?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -372,33 +379,47 @@ class NovelDownloaderService : Service() {
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
try {
|
||||
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@NovelDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(
|
||||
Dispatchers.IO
|
||||
) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
Logger.log("Downloading url $url")
|
||||
try {
|
||||
connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -473,7 +494,6 @@ class NovelDownloaderService : Service() {
|
||||
}
|
||||
|
||||
object NovelServiceDataSingleton {
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package ani.dantotsu.download.video
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.PlatformScheduler
|
||||
import androidx.media3.exoplayer.scheduler.Scheduler
|
||||
import ani.dantotsu.R
|
||||
|
||||
@UnstableApi
|
||||
class ExoplayerDownloadService :
|
||||
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||
companion object {
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||
}
|
||||
|
||||
override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
|
||||
|
||||
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
||||
|
||||
override fun getForegroundNotification(
|
||||
downloads: MutableList<Download>,
|
||||
notMetRequirements: Int
|
||||
): Notification =
|
||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||
this,
|
||||
R.drawable.mono,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
notMetRequirements
|
||||
)
|
||||
}
|
||||
@@ -7,15 +7,10 @@ import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
@@ -23,11 +18,8 @@ import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.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.defaultHeaders
|
||||
@@ -35,93 +27,101 @@ import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
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
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
fun startAnimeDownloadService(
|
||||
context: Context,
|
||||
title: String,
|
||||
episode: String,
|
||||
video: Video,
|
||||
subtitle: Subtitle? = null,
|
||||
sourceMedia: Media? = null,
|
||||
episodeImage: String? = null
|
||||
) {
|
||||
if (!isNotificationPermissionGranted(context)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context as Activity,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1
|
||||
)
|
||||
.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)
|
||||
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
|
||||
title,
|
||||
episode,
|
||||
video,
|
||||
subtitle,
|
||||
sourceMedia,
|
||||
episodeImage
|
||||
)
|
||||
|
||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||
val downloadCheck = downloadsManger
|
||||
.queryDownload(title, episode, MediaType.ANIME)
|
||||
|
||||
if (downloadCheck) {
|
||||
AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Exists")
|
||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||
.setPositiveButton("Yes") { _, _ ->
|
||||
PrefManager.getAnimeDownloadPreferences().edit()
|
||||
.remove(animeDownloadTask.getTaskName())
|
||||
.apply()
|
||||
downloadsManger.removeDownload(
|
||||
DownloadedType(
|
||||
title,
|
||||
episode,
|
||||
MediaType.ANIME
|
||||
)
|
||||
) {
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton("No") { _, _ -> }
|
||||
.show()
|
||||
} else {
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var download: DownloadManager? = null
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@UnstableApi
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
fun downloadManager(context: Context): DownloadManager {
|
||||
return download ?: let {
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
@@ -175,96 +175,7 @@ object Helper {
|
||||
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)
|
||||
fun startAnimeDownloadService(
|
||||
context: Context,
|
||||
title: String,
|
||||
episode: String,
|
||||
video: Video,
|
||||
subtitle: Subtitle? = null,
|
||||
sourceMedia: Media? = null,
|
||||
episodeImage: String? = null
|
||||
) {
|
||||
if (!isNotificationPermissionGranted(context)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context as Activity,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
|
||||
title,
|
||||
episode,
|
||||
video,
|
||||
subtitle,
|
||||
sourceMedia,
|
||||
episodeImage
|
||||
)
|
||||
|
||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||
val downloadCheck = downloadsManger
|
||||
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
||||
|
||||
if (downloadCheck) {
|
||||
AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Exists")
|
||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||
.setPositiveButton("Yes") { _, _ ->
|
||||
DownloadService.sendRemoveDownload(
|
||||
context,
|
||||
ExoplayerDownloadService::class.java,
|
||||
PrefManager.getAnimeDownloadPreferences().getString(
|
||||
animeDownloadTask.getTaskName(),
|
||||
""
|
||||
) ?: "",
|
||||
false
|
||||
)
|
||||
PrefManager.getAnimeDownloadPreferences().edit()
|
||||
.remove(animeDownloadTask.getTaskName())
|
||||
.apply()
|
||||
downloadsManger.removeDownload(
|
||||
DownloadedType(
|
||||
title,
|
||||
episode,
|
||||
DownloadedType.Type.ANIME
|
||||
)
|
||||
)
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
.setNegativeButton("No") { _, _ -> }
|
||||
.show()
|
||||
} else {
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
@OptIn(UnstableApi::class)
|
||||
fun getSimpleCache(context: Context): SimpleCache {
|
||||
return if (simpleCache == null) {
|
||||
@@ -276,14 +187,23 @@ object Helper {
|
||||
simpleCache!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
@Synchronized
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private fun getDownloadDirectory(context: Context): File {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(null)
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.filesDir
|
||||
}
|
||||
}
|
||||
return true
|
||||
return downloadDirectory!!
|
||||
}
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var download: DownloadManager? = null
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var simpleCache: SimpleCache? = null
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var downloadDirectory: File? = null
|
||||
}
|
||||
@@ -207,6 +207,21 @@ class AnimeFragment : Fragment() {
|
||||
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
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) {
|
||||
animePageAdapter.updateHeight()
|
||||
model.getTrending().observe(viewLifecycleOwner) {
|
||||
@@ -263,7 +278,7 @@ class AnimeFragment : Fragment() {
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending(1)
|
||||
model.loadUpdated()
|
||||
model.loadAll()
|
||||
model.loadPopular(
|
||||
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularAnimeList
|
||||
|
||||
@@ -4,12 +4,14 @@ import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -21,6 +23,7 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.CalendarActivity
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
@@ -41,6 +44,7 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
||||
val ready = MutableLiveData(false)
|
||||
lateinit var binding: ItemAnimePageBinding
|
||||
private lateinit var trendingBinding: LayoutTrendingBinding
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
@@ -53,14 +57,15 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
override fun onBindViewHolder(holder: AnimePageViewHolder, position: Int) {
|
||||
binding = holder.binding
|
||||
trendingViewPager = binding.animeTrendingViewPager
|
||||
trendingBinding = LayoutTrendingBinding.bind(binding.root)
|
||||
trendingViewPager = trendingBinding.trendingViewPager
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView =
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
@@ -69,16 +74,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = (-108f).px
|
||||
}
|
||||
|
||||
updateAvatar()
|
||||
|
||||
binding.animeSearchBar.hint = "ANIME"
|
||||
binding.animeSearchBarText.setOnClickListener {
|
||||
trendingBinding.searchBar.hint = "ANIME"
|
||||
trendingBinding.searchBarText.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
it.context,
|
||||
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
|
||||
@@ -86,26 +91,28 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.animeSearchBar.setEndIconOnClickListener {
|
||||
binding.animeSearchBarText.performClick()
|
||||
}
|
||||
|
||||
binding.animeUserAvatar.setSafeOnClickListener {
|
||||
trendingBinding.userAvatar.setSafeOnClickListener {
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
binding.animeUserAvatar.setOnLongClickListener { view ->
|
||||
trendingBinding.userAvatar.setOnLongClickListener { view ->
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
ContextCompat.startActivity(
|
||||
view.context,
|
||||
Intent(view.context, ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid),null
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
binding.animeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
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(
|
||||
binding.animePreviousSeason,
|
||||
@@ -134,8 +141,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.animeIncludeList.visibility =
|
||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||
binding.animeIncludeList.isVisible = Anilist.userid != null
|
||||
|
||||
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
|
||||
|
||||
@@ -159,30 +165,31 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
fun updateTrending(adaptor: MediaAdaptor) {
|
||||
binding.animeTrendingProgressBar.visibility = View.GONE
|
||||
binding.animeTrendingViewPager.adapter = adaptor
|
||||
binding.animeTrendingViewPager.offscreenPageLimit = 3
|
||||
binding.animeTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||
binding.animeTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
|
||||
trendingBinding.trendingProgressBar.visibility = View.GONE
|
||||
trendingBinding.trendingViewPager.adapter = adaptor
|
||||
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
||||
RecyclerView.OVER_SCROLL_NEVER
|
||||
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.animeTrendingViewPager.currentItem =
|
||||
binding.animeTrendingViewPager.currentItem + 1
|
||||
trendingBinding.trendingViewPager.currentItem += 1
|
||||
}
|
||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
||||
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
trendHandler!!.removeCallbacks(trendRun)
|
||||
trendHandler!!.postDelayed(trendRun, 4000)
|
||||
trendHandler?.removeCallbacks(trendRun)
|
||||
if (PrefManager.getVal(PrefName.TrendingScroller)) {
|
||||
trendHandler!!.postDelayed(trendRun, 4000)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.animeTrendingViewPager.layoutAnimation =
|
||||
trendingBinding.trendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animeTitleContainer.startAnimation(setSlideUp())
|
||||
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
||||
binding.animeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animeSeasonsCont.layoutAnimation =
|
||||
@@ -190,36 +197,83 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
fun updateRecent(adaptor: MediaAdaptor) {
|
||||
binding.animeUpdatedProgressBar.visibility = View.GONE
|
||||
binding.animeUpdatedRecyclerView.adapter = adaptor
|
||||
binding.animeUpdatedRecyclerView.layoutManager =
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeUpdatedRecyclerView,
|
||||
animeUpdatedProgressBar,
|
||||
animeRecently
|
||||
)
|
||||
animePopular.visibility = View.VISIBLE
|
||||
animePopular.startAnimation(setSlideUp())
|
||||
if (adaptor.itemCount == 0) {
|
||||
animeRecentlyContainer.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun updateMovies(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeMoviesRecyclerView,
|
||||
animeMoviesProgressBar,
|
||||
animeMovies
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeTopRatedRecyclerView,
|
||||
animeTopRatedProgressBar,
|
||||
animeTopRated
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeMostFavRecyclerView,
|
||||
animeMostFavProgressBar,
|
||||
animeMostFav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(
|
||||
binding.animeUpdatedRecyclerView.context,
|
||||
recyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeRecently.visibility = View.VISIBLE
|
||||
binding.animeRecently.startAnimation(setSlideUp())
|
||||
binding.animeUpdatedRecyclerView.layoutAnimation =
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp())
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animePopular.visibility = View.VISIBLE
|
||||
binding.animePopular.startAnimation(setSlideUp())
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
if (Anilist.avatar != null && ready.value == true) {
|
||||
binding.animeUserAvatar.loadImage(Anilist.avatar)
|
||||
binding.animeUserAvatar.imageTintList = null
|
||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||
trendingBinding.userAvatar.imageTintList = null
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotificationCount() {
|
||||
if (this::binding.isInitialized) {
|
||||
binding.animeNotificationCount.visibility =
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -78,10 +80,13 @@ class HomeFragment : Fragment() {
|
||||
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
||||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
|
||||
blurImage(binding.homeUserBg, Anilist.bg)
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
blurImage(
|
||||
if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen,
|
||||
Anilist.bg
|
||||
)
|
||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
|
||||
binding.homeAnimeList.setOnClickListener {
|
||||
@@ -123,9 +128,10 @@ class HomeFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
binding.homeUserAvatarContainer.setOnLongClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
ContextCompat.startActivity(
|
||||
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid),null
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
@@ -134,6 +140,7 @@ class HomeFragment : Fragment() {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
|
||||
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
var reached = false
|
||||
@@ -372,10 +379,11 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
|
||||
if (_binding != null) {
|
||||
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
super.onResume()
|
||||
|
||||
@@ -160,11 +160,37 @@ class MangaFragment : Fragment() {
|
||||
})
|
||||
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
|
||||
if (i == true) {
|
||||
model.getTrendingNovel().observe(viewLifecycleOwner) {
|
||||
model.getPopularNovel().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
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) {
|
||||
mangaPageAdapter.updateHeight()
|
||||
model.getTrending().observe(viewLifecycleOwner) {
|
||||
@@ -237,7 +263,7 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending()
|
||||
model.loadTrendingNovel()
|
||||
model.loadAll()
|
||||
model.loadPopular(
|
||||
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularMangaList
|
||||
|
||||
@@ -4,12 +4,14 @@ import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -21,6 +23,7 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
@@ -40,6 +43,7 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
||||
val ready = MutableLiveData(false)
|
||||
lateinit var binding: ItemMangaPageBinding
|
||||
private lateinit var trendingBinding: LayoutTrendingBinding
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
@@ -52,33 +56,34 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
|
||||
override fun onBindViewHolder(holder: MangaPageViewHolder, position: Int) {
|
||||
binding = holder.binding
|
||||
trendingViewPager = binding.mangaTrendingViewPager
|
||||
trendingBinding = LayoutTrendingBinding.bind(binding.root)
|
||||
trendingViewPager = trendingBinding.trendingViewPager
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView =
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = (-108f).px
|
||||
}
|
||||
|
||||
updateAvatar()
|
||||
binding.mangaNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
binding.mangaSearchBar.hint = "MANGA"
|
||||
binding.mangaSearchBarText.setOnClickListener {
|
||||
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
trendingBinding.searchBar.hint = "MANGA"
|
||||
trendingBinding.searchBarText.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
it.context,
|
||||
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
|
||||
@@ -86,22 +91,23 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
||||
trendingBinding.userAvatar.setSafeOnClickListener {
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
binding.mangaUserAvatar.setOnLongClickListener { view ->
|
||||
trendingBinding.userAvatar.setOnLongClickListener { view ->
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
ContextCompat.startActivity(
|
||||
view.context,
|
||||
Intent(view.context, ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid),null
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
binding.mangaSearchBar.setEndIconOnClickListener {
|
||||
binding.mangaSearchBarText.performClick()
|
||||
trendingBinding.searchBar.setEndIconOnClickListener {
|
||||
trendingBinding.searchBarText.performClick()
|
||||
}
|
||||
|
||||
binding.mangaGenreImage.loadImage("https://s4.anilist.co/file/anilistcdn/media/manga/banner/105778-wk5qQ7zAaTGl.jpg")
|
||||
@@ -125,8 +131,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.mangaIncludeList.visibility =
|
||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||
binding.mangaIncludeList.isVisible = Anilist.userid != null
|
||||
|
||||
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
|
||||
|
||||
@@ -148,63 +153,121 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
|
||||
fun updateTrending(adaptor: MediaAdaptor) {
|
||||
binding.mangaTrendingProgressBar.visibility = View.GONE
|
||||
binding.mangaTrendingViewPager.adapter = adaptor
|
||||
binding.mangaTrendingViewPager.offscreenPageLimit = 3
|
||||
binding.mangaTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendingBinding.trendingProgressBar.visibility = View.GONE
|
||||
trendingBinding.trendingViewPager.adapter = adaptor
|
||||
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
||||
RecyclerView.OVER_SCROLL_NEVER
|
||||
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.mangaTrendingViewPager.currentItem += 1
|
||||
trendingBinding.trendingViewPager.currentItem += 1
|
||||
}
|
||||
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
||||
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
trendHandler!!.removeCallbacks(trendRun)
|
||||
trendHandler!!.postDelayed(trendRun, 4000)
|
||||
trendHandler?.removeCallbacks(trendRun)
|
||||
if (PrefManager.getVal(PrefName.TrendingScroller))
|
||||
trendHandler!!.postDelayed(trendRun, 4000)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.mangaTrendingViewPager.layoutAnimation =
|
||||
trendingBinding.trendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.mangaTitleContainer.startAnimation(setSlideUp())
|
||||
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
||||
binding.mangaListContainer.layoutAnimation =
|
||||
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) {
|
||||
binding.mangaNovelProgressBar.visibility = View.GONE
|
||||
binding.mangaNovelRecyclerView.adapter = adaptor
|
||||
binding.mangaNovelRecyclerView.layoutManager =
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaNovelRecyclerView,
|
||||
mangaNovelProgressBar,
|
||||
mangaNovel
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTopRatedRecyclerView,
|
||||
mangaTopRatedProgressBar,
|
||||
mangaTopRated
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaMostFavRecyclerView,
|
||||
mangaMostFavProgressBar,
|
||||
mangaMostFav
|
||||
)
|
||||
mangaPopular.visibility = View.VISIBLE
|
||||
mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
}
|
||||
|
||||
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(
|
||||
binding.mangaNovelRecyclerView.context,
|
||||
recyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.mangaNovel.visibility = View.VISIBLE
|
||||
binding.mangaNovel.startAnimation(setSlideUp())
|
||||
binding.mangaNovelRecyclerView.layoutAnimation =
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp())
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.mangaPopular.visibility = View.VISIBLE
|
||||
binding.mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
if (Anilist.avatar != null && ready.value == true) {
|
||||
binding.mangaUserAvatar.loadImage(Anilist.avatar)
|
||||
binding.mangaUserAvatar.imageTintList = null
|
||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||
trendingBinding.userAvatar.imageTintList = null
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotificationCount() {
|
||||
if (this::binding.isInitialized) {
|
||||
binding.mangaNotificationCount.visibility =
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ package ani.dantotsu.media
|
||||
import java.io.Serializable
|
||||
|
||||
data class Author(
|
||||
val id: Int,
|
||||
val name: String?,
|
||||
val image: String?,
|
||||
val role: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
|
||||
var id: Int,
|
||||
var name: String?,
|
||||
var image: String?,
|
||||
var role: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
|
||||
var character: ArrayList<Character>? = null
|
||||
) : Serializable
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.EmptyAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
@@ -32,7 +33,6 @@ class AuthorActivity : AppCompatActivity() {
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
private var author: Author? = null
|
||||
private var loaded = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -55,14 +55,15 @@ class AuthorActivity : AppCompatActivity() {
|
||||
binding.studioClose.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
model.getAuthor().observe(this) {
|
||||
if (it != null) {
|
||||
author = it
|
||||
loaded = true
|
||||
binding.studioProgressBar.visibility = View.GONE
|
||||
binding.studioRecycler.visibility = View.VISIBLE
|
||||
|
||||
if (author!!.yearMedia.isNullOrEmpty()) {
|
||||
binding.studioRecycler.visibility = View.GONE
|
||||
}
|
||||
val titlePosition = arrayListOf<Int>()
|
||||
val concatAdapter = ConcatAdapter()
|
||||
val map = author!!.yearMedia ?: return@observe
|
||||
@@ -89,9 +90,19 @@ class AuthorActivity : AppCompatActivity() {
|
||||
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
|
||||
concatAdapter.addAdapter(EmptyAdapter(empty))
|
||||
}
|
||||
|
||||
binding.studioRecycler.adapter = concatAdapter
|
||||
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) }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
@@ -16,7 +15,7 @@ import ani.dantotsu.setAnimation
|
||||
import java.io.Serializable
|
||||
|
||||
class AuthorAdapter(
|
||||
private val authorList: ArrayList<Author>
|
||||
private val authorList: ArrayList<Author>,
|
||||
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
|
||||
val binding =
|
||||
@@ -24,8 +23,7 @@ class AuthorAdapter(
|
||||
return AuthorViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder:AuthorViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val author = authorList[position]
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -16,8 +14,8 @@ import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
@@ -34,7 +32,6 @@ class CalendarActivity : AppCompatActivity() {
|
||||
private var selectedTabIdx = 1
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -45,13 +42,6 @@ class CalendarActivity : AppCompatActivity() {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
val primaryColor = typedValue.data
|
||||
val typedValue2 = TypedValue()
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue2,
|
||||
true
|
||||
)
|
||||
val titleTextColor = typedValue2.data
|
||||
val typedValue3 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||
val primaryTextColor = typedValue3.data
|
||||
@@ -74,10 +64,7 @@ class CalendarActivity : AppCompatActivity() {
|
||||
} else {
|
||||
binding.root.fitsSystemWindows = false
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
hideSystemBarsExtendView()
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ data class Character(
|
||||
var age: String? = null,
|
||||
var gender: String? = null,
|
||||
var dateOfBirth: FuzzyDate? = null,
|
||||
var roles: ArrayList<Media>? = null
|
||||
var roles: ArrayList<Media>? = null,
|
||||
val voiceActor: ArrayList<Author>? = null,
|
||||
) : Serializable
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
@@ -24,12 +23,13 @@ class CharacterAdapter(
|
||||
return CharacterViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val character = characterList[position]
|
||||
binding.itemCompactRelation.text = character.role + " "
|
||||
val whitespace = "${character.role} "
|
||||
character.voiceActor
|
||||
binding.itemCompactRelation.text = whitespace
|
||||
binding.itemCompactImage.loadImage(character.image)
|
||||
binding.itemCompactTitle.text = character.name
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -94,7 +95,8 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
character.isFav = Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
|
||||
character.isFav =
|
||||
Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.characterFav.setImageResource(
|
||||
@@ -152,7 +154,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.characterProgress.isGone = loaded
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
@@ -20,23 +21,36 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
||||
return GenreViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val desc =
|
||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
||||
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
||||
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||
"Male" -> currActivity()!!.getString(R.string.male)
|
||||
"Female" -> currActivity()!!.getString(R.string.female)
|
||||
else -> character.gender
|
||||
} else "") + "\n" + character.description
|
||||
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
|
||||
(if (character.dateOfBirth.toString() != "")
|
||||
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
|
||||
(if (character.gender != "null")
|
||||
currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||
currActivity()!!.getString(R.string.male) -> currActivity()!!.getString(
|
||||
R.string.male
|
||||
)
|
||||
|
||||
currActivity()!!.getString(R.string.female) -> currActivity()!!.getString(
|
||||
R.string.female
|
||||
)
|
||||
|
||||
else -> character.gender
|
||||
} else "") + "\n" + character.description
|
||||
|
||||
binding.characterDesc.isTextSelectable
|
||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(SpoilerPlugin()).build()
|
||||
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
|
||||
|
||||
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
|
||||
binding.voiceActorRecycler.layoutManager = LinearLayoutManager(
|
||||
activity, LinearLayoutManager.HORIZONTAL, false
|
||||
)
|
||||
if (binding.voiceActorRecycler.adapter!!.itemCount == 0) {
|
||||
binding.voiceActorContainer.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
@@ -7,6 +7,7 @@ import ani.dantotsu.connections.anilist.api.MediaList
|
||||
import ani.dantotsu.connections.anilist.api.MediaType
|
||||
import ani.dantotsu.media.anime.Anime
|
||||
import ani.dantotsu.media.manga.Manga
|
||||
import ani.dantotsu.profile.User
|
||||
import java.io.Serializable
|
||||
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
|
||||
|
||||
@@ -25,7 +26,7 @@ data class Media(
|
||||
var cover: String? = null,
|
||||
var banner: String? = null,
|
||||
var relation: String? = null,
|
||||
var popularity: Int? = null,
|
||||
var favourites: Int? = null,
|
||||
|
||||
var isAdult: Boolean,
|
||||
var isFav: Boolean = false,
|
||||
@@ -56,6 +57,9 @@ data class Media(
|
||||
var trailer: String? = null,
|
||||
var startDate: FuzzyDate? = null,
|
||||
var endDate: FuzzyDate? = null,
|
||||
var popularity: Int? = null,
|
||||
|
||||
var timeUntilAiring: Long? = null,
|
||||
|
||||
var characters: ArrayList<Character>? = null,
|
||||
var staff: ArrayList<Author>? = null,
|
||||
@@ -63,7 +67,7 @@ data class Media(
|
||||
var sequel: Media? = null,
|
||||
var relations: ArrayList<Media>? = null,
|
||||
var recommendations: ArrayList<Media>? = null,
|
||||
|
||||
var users: ArrayList<User>? = null,
|
||||
var vrvId: String? = null,
|
||||
var crunchySlug: String? = null,
|
||||
|
||||
@@ -83,7 +87,7 @@ data class Media(
|
||||
name = apiMedia.title!!.english,
|
||||
nameRomaji = apiMedia.title!!.romaji,
|
||||
userPreferredName = apiMedia.title!!.userPreferred,
|
||||
cover = apiMedia.coverImage?.large,
|
||||
cover = apiMedia.coverImage?.large ?: apiMedia.coverImage?.medium,
|
||||
banner = apiMedia.bannerImage,
|
||||
status = apiMedia.status.toString(),
|
||||
isFav = apiMedia.isFavourite!!,
|
||||
@@ -95,6 +99,8 @@ data class Media(
|
||||
meanScore = apiMedia.meanScore,
|
||||
startDate = apiMedia.startDate,
|
||||
endDate = apiMedia.endDate,
|
||||
favourites = apiMedia.favourites,
|
||||
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000 },
|
||||
anime = if (apiMedia.type == MediaType.ANIME) Anime(
|
||||
totalEpisodes = apiMedia.episodes,
|
||||
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
|
||||
@@ -109,7 +115,8 @@ data class Media(
|
||||
this.userScore = mediaList.score?.toInt() ?: 0
|
||||
this.userStatus = mediaList.status?.toString()
|
||||
this.userUpdatedAt = mediaList.updatedAt?.toLong()
|
||||
this.genres = mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
|
||||
this.genres =
|
||||
mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
|
||||
}
|
||||
|
||||
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
@@ -15,25 +13,25 @@ import android.widget.ImageView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.ItemMediaCompactBinding
|
||||
import ani.dantotsu.databinding.ItemMediaLargeBinding
|
||||
import ani.dantotsu.databinding.ItemMediaPageBinding
|
||||
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.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 jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import java.io.Serializable
|
||||
|
||||
|
||||
@@ -85,7 +83,7 @@ class MediaAdaptor(
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (type) {
|
||||
0 -> {
|
||||
@@ -94,8 +92,8 @@ class MediaAdaptor(
|
||||
val media = mediaList?.getOrNull(position)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
@@ -140,8 +138,8 @@ class MediaAdaptor(
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
@@ -151,25 +149,30 @@ class MediaAdaptor(
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
)
|
||||
if (media.anime != null) {
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||
val itemTotal = " " + if ((media.anime.totalEpisodes
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.episode_plural)
|
||||
else currActivity()!!.getString(R.string.episode_singular)
|
||||
) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(
|
||||
R.string.episode_singular
|
||||
)
|
||||
b.itemTotal.text = itemTotal
|
||||
b.itemCompactTotal.text =
|
||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||
?: "??").toString()) else (media.anime.totalEpisodes
|
||||
?: "??").toString()
|
||||
} else if (media.manga != null) {
|
||||
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||
val itemTotal = " " + if ((media.manga.totalChapters
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.chapter_plural)
|
||||
else currActivity()!!.getString(R.string.chapter_singular)
|
||||
) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(
|
||||
R.string.chapter_singular
|
||||
)
|
||||
b.itemTotal.text = itemTotal
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
|
||||
val start = mediaList.size
|
||||
mediaList.addAll(mediaList)
|
||||
notifyDataSetChanged()
|
||||
val end = mediaList.size - start
|
||||
notifyItemRangeInserted(start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +181,7 @@ class MediaAdaptor(
|
||||
val b = (holder as MediaPageViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
if (bannerAnimations)
|
||||
@@ -187,9 +191,12 @@ class MediaAdaptor(
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
blurImage(
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
|
||||
media.banner ?: media.cover
|
||||
)
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
@@ -236,9 +243,12 @@ class MediaAdaptor(
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
blurImage(
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
|
||||
media.banner ?: media.cover
|
||||
)
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
|
||||
@@ -2,9 +2,8 @@ package ani.dantotsu.media
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.TypedValue
|
||||
@@ -12,18 +11,19 @@ import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.color
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updateMargins
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -54,6 +54,7 @@ import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.LauncherWrapper
|
||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -62,20 +63,21 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||
|
||||
lateinit var launcher: LauncherWrapper
|
||||
lateinit var binding: ActivityMediaBinding
|
||||
private val scope = lifecycleScope
|
||||
private val model: MediaDetailsViewModel by viewModels()
|
||||
lateinit var tabLayout: TripleNavAdapter
|
||||
var selected = 0
|
||||
lateinit var navBar: AnimatedBottomBar
|
||||
var anime = true
|
||||
private var adult = false
|
||||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||
@@ -83,8 +85,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
if (id != -1) {
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
media =
|
||||
Anilist.query.getMedia(id, false) ?: emptyMedia()
|
||||
media = Anilist.query.getMedia(id, false) ?: emptyMedia()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +94,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return
|
||||
}
|
||||
val contract = ActivityResultContracts.OpenDocumentTree()
|
||||
launcher = LauncherWrapper(this, contract)
|
||||
|
||||
mediaSingleton = null
|
||||
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||
MediaSingleton.bitmap = null
|
||||
@@ -100,26 +104,38 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
navBar = binding.mediaBottomBar
|
||||
|
||||
val isVertical = resources.configuration.orientation
|
||||
//Ui init
|
||||
// Ui init
|
||||
|
||||
initActivity(this)
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
val oldMargin = binding.mediaViewPager.marginBottom
|
||||
AndroidBug5497Workaround.assistActivity(this) {
|
||||
if (it) {
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = 0
|
||||
}
|
||||
binding.mediaTabContainer.visibility = View.GONE
|
||||
navBar.visibility = View.GONE
|
||||
} else {
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = oldMargin
|
||||
}
|
||||
binding.mediaTabContainer.visibility = View.VISIBLE
|
||||
navBar.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
val navBarRightMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) navBarHeight else 0
|
||||
val navBarBottomMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) 0 else navBarHeight
|
||||
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
rightMargin = navBarRightMargin
|
||||
bottomMargin = navBarBottomMargin
|
||||
}
|
||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
@@ -147,7 +163,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
val banner =
|
||||
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||
val viewPager = binding.mediaViewPager
|
||||
//tabLayout = binding.mediaTab as AnimatedBottomBar
|
||||
viewPager.isUserInputEnabled = false
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
|
||||
@@ -157,9 +172,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
binding.mediaCoverImage.loadImage(media.cover)
|
||||
binding.mediaCoverImage.setOnLongClickListener {
|
||||
val coverTitle = "${media.userPreferredName}[Cover]"
|
||||
ImageViewDialog.newInstance(
|
||||
this,
|
||||
media.userPreferredName + "[Cover]",
|
||||
coverTitle,
|
||||
media.cover
|
||||
)
|
||||
}
|
||||
@@ -176,9 +192,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
|
||||
override fun onLongClick(event: MotionEvent) {
|
||||
val bannerTitle = "${media.userPreferredName}[Banner]"
|
||||
ImageViewDialog.newInstance(
|
||||
this@MediaDetailsActivity,
|
||||
media.userPreferredName + "[Banner]",
|
||||
bannerTitle,
|
||||
media.banner ?: media.cover
|
||||
)
|
||||
banner.performClick()
|
||||
@@ -186,7 +203,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
})
|
||||
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||
binding.mediaTitle.text = " ${media.userPreferredName}"
|
||||
val mediaTitle = " ${media.userPreferredName}"
|
||||
binding.mediaTitle.text = mediaTitle
|
||||
binding.incognito.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.mediaTitle.text = media.userPreferredName
|
||||
@@ -210,20 +228,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
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(
|
||||
scope,
|
||||
@@ -231,7 +235,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
R.drawable.ic_round_favorite_24,
|
||||
R.drawable.ic_round_favorite_border_24,
|
||||
R.color.bg_opp,
|
||||
R.color.violet_400,//TODO: Change to colorSecondary
|
||||
R.color.violet_400,
|
||||
media.isFav
|
||||
) {
|
||||
media.isFav = it
|
||||
@@ -246,13 +250,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
@SuppressLint("ResourceType")
|
||||
fun total() {
|
||||
val text = SpannableStringBuilder().apply {
|
||||
val typedValue = TypedValue()
|
||||
val mediaTypedValue = TypedValue()
|
||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue,
|
||||
mediaTypedValue,
|
||||
true
|
||||
)
|
||||
val white = typedValue.data
|
||||
val white = mediaTypedValue.data
|
||||
if (media.userStatus != null) {
|
||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||
val typedValue = TypedValue()
|
||||
@@ -342,18 +346,16 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
progress()
|
||||
}
|
||||
}
|
||||
tabLayout = TripleNavAdapter(
|
||||
binding.mediaTab1,
|
||||
binding.mediaTab2,
|
||||
binding.mediaTab3,
|
||||
media.anime != null,
|
||||
media.format ?: "",
|
||||
isVertical == 1
|
||||
)
|
||||
adult = media.isAdult
|
||||
if (media.anime != null) {
|
||||
viewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME, media, intent.getIntExtra("commentId", -1))
|
||||
ViewPagerAdapter(
|
||||
supportFragmentManager,
|
||||
lifecycle,
|
||||
SupportedMedia.ANIME,
|
||||
media,
|
||||
intent.getIntExtra("commentId", -1)
|
||||
)
|
||||
} else if (media.manga != null) {
|
||||
viewPager.adapter = ViewPagerAdapter(
|
||||
supportFragmentManager,
|
||||
@@ -365,31 +367,47 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
anime = false
|
||||
}
|
||||
|
||||
|
||||
selected = media.selected!!.window
|
||||
binding.mediaTitle.translationX = -screenWidth
|
||||
|
||||
tabLayout.selectionListener = { selected, newId ->
|
||||
binding.commentInputLayout.visibility = if (selected == 2) View.VISIBLE else View.GONE
|
||||
this.selected = selected
|
||||
selectFromID(newId)
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
val sel = model.loadSelected(media, isDownload)
|
||||
sel.window = selected
|
||||
model.saveSelected(media.id, sel)
|
||||
val infoTab = navBar.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
|
||||
val watchTab = if (anime) {
|
||||
navBar.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
|
||||
} else if (media.format == "NOVEL") {
|
||||
navBar.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
|
||||
} else {
|
||||
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
|
||||
}
|
||||
tabLayout.selectTab(selected)
|
||||
selectFromID(tabLayout.selected)
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
|
||||
val commentTab =
|
||||
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
|
||||
navBar.addTab(infoTab)
|
||||
navBar.addTab(watchTab)
|
||||
navBar.addTab(commentTab)
|
||||
if (model.continueMedia == null && media.cameFromContinue) {
|
||||
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
||||
selected = 1
|
||||
}
|
||||
val frag = intent.getStringExtra("FRAGMENT_TO_LOAD")
|
||||
if (frag != null) {
|
||||
selected = 2
|
||||
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) }
|
||||
live.observe(this) {
|
||||
@@ -402,40 +420,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectFromID(id: Int) {
|
||||
when (id) {
|
||||
R.id.info -> {
|
||||
selected = 0
|
||||
}
|
||||
|
||||
R.id.watch, R.id.read -> {
|
||||
selected = 1
|
||||
}
|
||||
|
||||
R.id.comment -> {
|
||||
selected = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun idFromSelect(): Int {
|
||||
if (anime) when (selected) {
|
||||
0 -> return R.id.info
|
||||
1 -> return R.id.watch
|
||||
2 -> return R.id.comment
|
||||
}
|
||||
else when (selected) {
|
||||
0 -> return R.id.info
|
||||
1 -> return R.id.read
|
||||
2 -> return R.id.comment
|
||||
}
|
||||
return R.id.info
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val rightMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) navBarHeight else 0
|
||||
val bottomMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) 0 else navBarHeight
|
||||
val params: ViewGroup.MarginLayoutParams =
|
||||
navBar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.updateMargins(right = rightMargin, bottom = bottomMargin)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (this::tabLayout.isInitialized) {
|
||||
tabLayout.selectTab(selected)
|
||||
}
|
||||
navBar.selectTabAt(selected)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@@ -443,7 +442,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
ANIME, MANGA, NOVEL
|
||||
}
|
||||
|
||||
//ViewPager
|
||||
// ViewPager
|
||||
private class ViewPagerAdapter(
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle,
|
||||
@@ -462,6 +461,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
SupportedMedia.MANGA -> MangaReadFragment()
|
||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val fragment = CommentsFragment()
|
||||
val bundle = Bundle()
|
||||
@@ -489,13 +489,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
binding.mediaCover.visibility =
|
||||
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
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) {
|
||||
isCollapsed = true
|
||||
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
@@ -29,6 +28,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
@@ -52,26 +52,23 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
it
|
||||
}
|
||||
if (isDownload) {
|
||||
data.sourceIndex = if (media.anime != null) {
|
||||
AnimeSources.list.size - 1
|
||||
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
||||
MangaSources.list.size - 1
|
||||
} else {
|
||||
NovelSources.list.size - 1
|
||||
data.sourceIndex = when {
|
||||
media.anime != null -> {
|
||||
AnimeSources.list.size - 1
|
||||
}
|
||||
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
|
||||
MangaSources.list.size - 1
|
||||
}
|
||||
|
||||
else -> {
|
||||
NovelSources.list.size - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
private var loading = false
|
||||
|
||||
@@ -152,10 +149,10 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
watchSources?.get(i)?.apply {
|
||||
if (!post && !allowsPreloading) return@apply
|
||||
ep.sEpisode?.let {
|
||||
loadByVideoServers(link, ep.extra, it) {
|
||||
if (it.videos.isNotEmpty()) {
|
||||
list.add(it)
|
||||
ep.extractorCallback?.invoke(it)
|
||||
loadByVideoServers(link, ep.extra, it) { extractor ->
|
||||
if (extractor.videos.isNotEmpty()) {
|
||||
list.add(extractor)
|
||||
ep.extractorCallback?.invoke(extractor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,7 +288,6 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
suspend fun loadMangaChapterImages(
|
||||
chapter: MangaChapter,
|
||||
selected: Selected,
|
||||
series: String,
|
||||
post: Boolean = true
|
||||
): Boolean {
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package ani.dantotsu.media
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
@@ -16,16 +15,33 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||
import ani.dantotsu.databinding.*
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||
import ani.dantotsu.databinding.FragmentMediaInfoBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemQuelsBinding
|
||||
import ani.dantotsu.databinding.ItemTitleChipgroupBinding
|
||||
import ani.dantotsu.databinding.ItemTitleRecyclerBinding
|
||||
import ani.dantotsu.databinding.ItemTitleSearchBinding
|
||||
import ani.dantotsu.databinding.ItemTitleTextBinding
|
||||
import ani.dantotsu.databinding.ItemTitleTrailerBinding
|
||||
import ani.dantotsu.displayTimer
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import io.noties.markwon.Markwon
|
||||
@@ -37,7 +53,6 @@ import java.io.Serializable
|
||||
import java.net.URLEncoder
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
class MediaInfoFragment : Fragment() {
|
||||
private var _binding: FragmentMediaInfoBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
@@ -46,6 +61,8 @@ class MediaInfoFragment : Fragment() {
|
||||
private var type = "ANIME"
|
||||
private val genreModel: GenresViewModel by activityViewModels()
|
||||
|
||||
private val tripleTab = "\t\t\t"
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -63,8 +80,8 @@ class MediaInfoFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val model: MediaDetailsViewModel by activityViewModels()
|
||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoProgressBar.isGone = loaded
|
||||
binding.mediaInfoContainer.isVisible = loaded
|
||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||
|
||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||
@@ -75,17 +92,18 @@ class MediaInfoFragment : Fragment() {
|
||||
if (media != null && !loaded) {
|
||||
loaded = true
|
||||
|
||||
|
||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||
binding.mediaInfoContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
|
||||
val infoName = tripleTab + (media.name ?: media.nameRomaji)
|
||||
binding.mediaInfoName.text = infoName
|
||||
binding.mediaInfoName.setOnLongClickListener {
|
||||
copyToClipboard(media.name ?: media.nameRomaji)
|
||||
true
|
||||
}
|
||||
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
|
||||
View.VISIBLE
|
||||
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
||||
val infoNameRomanji = tripleTab + media.nameRomaji
|
||||
binding.mediaInfoNameRomaji.text = infoNameRomanji
|
||||
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
||||
copyToClipboard(media.nameRomaji)
|
||||
true
|
||||
@@ -97,6 +115,8 @@ class MediaInfoFragment : Fragment() {
|
||||
binding.mediaInfoSource.text = media.source
|
||||
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
||||
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
||||
binding.mediaInfoPopularity.text = media.popularity.toString()
|
||||
binding.mediaInfoFavorites.text = media.favourites.toString()
|
||||
if (media.anime != null) {
|
||||
val episodeDuration = media.anime.episodeDuration
|
||||
|
||||
@@ -125,8 +145,10 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeason.text =
|
||||
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
|
||||
val seasonInfo =
|
||||
"${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
|
||||
binding.mediaInfoSeason.text = seasonInfo
|
||||
|
||||
if (media.anime.mainStudio != null) {
|
||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||
@@ -160,9 +182,12 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||
binding.mediaInfoTotal.text =
|
||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
|
||||
?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString()
|
||||
val infoTotal = if (media.anime.nextAiringEpisode != null)
|
||||
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
|
||||
else
|
||||
(media.anime.totalEpisodes ?: "~").toString()
|
||||
binding.mediaInfoTotal.text = infoTotal
|
||||
|
||||
} else if (media.manga != null) {
|
||||
type = "MANGA"
|
||||
binding.mediaInfoTotalTitle.setText(R.string.total_chaps)
|
||||
@@ -189,8 +214,10 @@ class MediaInfoFragment : Fragment() {
|
||||
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
binding.mediaInfoDescription.text =
|
||||
"\t\t\t" + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
|
||||
val infoDesc =
|
||||
tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
|
||||
binding.mediaInfoDescription.text = infoDesc
|
||||
|
||||
binding.mediaInfoDescription.setOnClickListener {
|
||||
if (binding.mediaInfoDescription.maxLines == 5) {
|
||||
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100)
|
||||
@@ -200,8 +227,7 @@ class MediaInfoFragment : Fragment() {
|
||||
.setDuration(400).start()
|
||||
}
|
||||
}
|
||||
|
||||
countDown(media, binding.mediaInfoContainer)
|
||||
displayTimer(media, binding.mediaInfoContainer)
|
||||
val parent = _binding?.mediaInfoContainer!!
|
||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
|
||||
@@ -413,113 +439,155 @@ class MediaInfoFragment : Fragment() {
|
||||
|
||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||
if (media.sequel != null || media.prequel != null) {
|
||||
val bind = ItemQuelsBinding.inflate(
|
||||
ItemQuelsBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
|
||||
if (media.sequel != null) {
|
||||
bind.mediaInfoSequel.visibility = View.VISIBLE
|
||||
bind.mediaInfoSequelImage.loadImage(
|
||||
media.sequel!!.banner ?: media.sequel!!.cover
|
||||
)
|
||||
bind.mediaInfoSequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.sequel as Serializable
|
||||
), null
|
||||
if (media.sequel != null) {
|
||||
mediaInfoSequel.visibility = View.VISIBLE
|
||||
mediaInfoSequelImage.loadImage(
|
||||
media.sequel!!.banner ?: media.sequel!!.cover
|
||||
)
|
||||
}
|
||||
}
|
||||
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(
|
||||
mediaInfoSequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.prequel as Serializable
|
||||
), null
|
||||
)
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.sequel as Serializable
|
||||
), null
|
||||
)
|
||||
}
|
||||
}
|
||||
if (media.prequel != null) {
|
||||
mediaInfoPrequel.visibility = View.VISIBLE
|
||||
mediaInfoPrequelImage.loadImage(
|
||||
media.prequel!!.banner ?: media.prequel!!.cover
|
||||
)
|
||||
mediaInfoPrequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.prequel as Serializable
|
||||
), null
|
||||
)
|
||||
}
|
||||
}
|
||||
parent.addView(root)
|
||||
}
|
||||
|
||||
ItemTitleSearchBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
).apply {
|
||||
|
||||
titleSearchImage.loadImage(media.banner ?: media.cover)
|
||||
titleSearchText.text =
|
||||
getString(R.string.search_title, media.mainName())
|
||||
titleSearchCard.setSafeOnClickListener {
|
||||
val query = Intent(requireContext(), SearchActivity::class.java)
|
||||
.putExtra("type", "ANIME")
|
||||
.putExtra("query", media.mainName())
|
||||
.putExtra("search", true)
|
||||
ContextCompat.startActivity(requireContext(), query, null)
|
||||
}
|
||||
|
||||
parent.addView(root)
|
||||
}
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
val bindi = ItemTitleRecyclerBinding.inflate(
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
|
||||
bindi.itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.relations!!, requireActivity())
|
||||
bindi.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bindi.root)
|
||||
itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.relations!!, requireActivity())
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
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)
|
||||
).apply {
|
||||
itemTitle.setText(R.string.characters)
|
||||
itemRecycler.adapter =
|
||||
CharacterAdapter(media.characters!!)
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.staff.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.staff)
|
||||
bind.itemRecycler.adapter =
|
||||
AuthorAdapter(media.staff!!)
|
||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bind.root)
|
||||
).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) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.recommended)
|
||||
bind.itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
).apply {
|
||||
itemTitle.setText(R.string.recommended)
|
||||
itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.users.isNullOrEmpty() && !offline) {
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
parent.addView(bind.root)
|
||||
).apply {
|
||||
itemTitle.setText(R.string.social)
|
||||
itemRecycler.adapter =
|
||||
MediaSocialAdapter(media.users!!)
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -544,11 +612,12 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onViewCreated(view, null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoProgressBar.isGone = loaded
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter.LengthFilter
|
||||
import android.view.Gravity
|
||||
@@ -11,11 +10,18 @@ import android.widget.ArrayAdapter
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.DatePickerFragment
|
||||
import ani.dantotsu.InputFilterMinMax
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -36,7 +42,6 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
var media: Media?
|
||||
@@ -168,9 +173,10 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
val init =
|
||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||
.toInt() else 0
|
||||
if (init < (total
|
||||
?: 5000)
|
||||
) binding.mediaListProgress.setText((init + 1).toString())
|
||||
if (init < (total ?: 5000)) {
|
||||
val progressText = "${init + 1}"
|
||||
binding.mediaListProgress.setText(progressText)
|
||||
}
|
||||
if (init + 1 == (total ?: 5000)) {
|
||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||
onComplete()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter.LengthFilter
|
||||
import android.view.Gravity
|
||||
@@ -10,11 +9,16 @@ import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.InputFilterMinMax
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.snackString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -54,7 +58,6 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
val scope = viewLifecycleOwner.lifecycleScope
|
||||
@@ -68,7 +71,7 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString("Failed to delete because of... ${e.message}")
|
||||
snackString(getString(R.string.delete_fail_reason, e.message))
|
||||
}
|
||||
return@withContext
|
||||
}
|
||||
@@ -154,7 +157,10 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
val init =
|
||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||
.toInt() else 0
|
||||
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
|
||||
if (init < (total ?: 5000)) {
|
||||
val progressText = "${init + 1}"
|
||||
binding.mediaListProgress.setText(progressText)
|
||||
}
|
||||
if (init + 1 == (total ?: 5000)) {
|
||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||
}
|
||||
|
||||
146
app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
Normal file
146
app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
Normal file
@@ -0,0 +1,146 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import java.util.Locale
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object MediaNameAdapter {
|
||||
|
||||
private const val REGEX_ITEM = "[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
|
||||
private const val REGEX_PART_NUMBER = "(?<!part\\s)\\b(\\d+)\\b"
|
||||
private const val REGEX_EPISODE =
|
||||
"(episode|episodio|ep|e)${REGEX_ITEM}\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
private const val REGEX_SEASON = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
private const val REGEX_SUBDUB = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
||||
private const val REGEX_CHAPTER = "(chapter|chap|ch|c)${REGEX_ITEM}"
|
||||
|
||||
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
||||
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val soft = subdubMatcher.group(1)
|
||||
val subdub = subdubMatcher.group(2)
|
||||
val bed = subdubMatcher.group(3) ?: ""
|
||||
|
||||
val toggled = when (typeToSetTo) {
|
||||
SubDubType.SUB -> "sub"
|
||||
SubDubType.DUB -> "dub"
|
||||
SubDubType.NULL -> ""
|
||||
}
|
||||
val toggledCasePreserved =
|
||||
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
|
||||
?.isUpperCase() == true
|
||||
) toggled.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(
|
||||
Locale.ROOT
|
||||
) else it.toString()
|
||||
} else toggled
|
||||
|
||||
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubDub(text: String): SubDubType {
|
||||
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
|
||||
when (subdub) {
|
||||
"sub" -> SubDubType.SUB
|
||||
"dub" -> SubDubType.DUB
|
||||
else -> SubDubType.NULL
|
||||
}
|
||||
} else {
|
||||
SubDubType.NULL
|
||||
}
|
||||
}
|
||||
|
||||
enum class SubDubType {
|
||||
SUB, DUB, NULL
|
||||
}
|
||||
|
||||
fun findSeasonNumber(text: String): Int? {
|
||||
val seasonPattern: Pattern = Pattern.compile(REGEX_SEASON, Pattern.CASE_INSENSITIVE)
|
||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||
|
||||
return if (seasonMatcher.find()) {
|
||||
seasonMatcher.group(2)?.toInt()
|
||||
} else {
|
||||
text.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun findEpisodeNumber(text: String): Float? {
|
||||
val episodePattern: Pattern = Pattern.compile(REGEX_EPISODE, Pattern.CASE_INSENSITIVE)
|
||||
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||
|
||||
return if (episodeMatcher.find()) {
|
||||
if (episodeMatcher.group(2) != null) {
|
||||
episodeMatcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedEpisodeNumberPattern: Pattern =
|
||||
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
|
||||
val failedEpisodeNumberMatcher: Matcher =
|
||||
failedEpisodeNumberPattern.matcher(text)
|
||||
if (failedEpisodeNumberMatcher.find()) {
|
||||
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
text.toFloatOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEpisodeNumber(text: String): String {
|
||||
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||
text
|
||||
}
|
||||
val letterPattern = Regex("[a-zA-Z]")
|
||||
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||
removedNumber
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeEpisodeNumberCompletely(text: String): String {
|
||||
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "")
|
||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||
val failedEpisodeNumberPattern =
|
||||
Regex(REGEX_PART_NUMBER, RegexOption.IGNORE_CASE)
|
||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||
}
|
||||
} else {
|
||||
removedNumber
|
||||
}
|
||||
}
|
||||
|
||||
fun findChapterNumber(text: String): Float? {
|
||||
val pattern: Pattern = Pattern.compile(REGEX_CHAPTER, Pattern.CASE_INSENSITIVE)
|
||||
val matcher: Matcher = pattern.matcher(text)
|
||||
|
||||
return if (matcher.find()) {
|
||||
matcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedChapterNumberPattern: Pattern =
|
||||
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
|
||||
val failedChapterNumberMatcher: Matcher =
|
||||
failedChapterNumberPattern.matcher(text)
|
||||
if (failedChapterNumberMatcher.find()) {
|
||||
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
text.toFloatOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt
Normal file
70
app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt
Normal file
@@ -0,0 +1,70 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemFollowerGridBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.setAnimation
|
||||
|
||||
class MediaSocialAdapter(private val user: ArrayList<User>) :
|
||||
RecyclerView.Adapter<MediaSocialAdapter.DeveloperViewHolder>() {
|
||||
|
||||
inner class DeveloperViewHolder(val binding: ItemFollowerGridBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeveloperViewHolder {
|
||||
return DeveloperViewHolder(
|
||||
ItemFollowerGridBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: DeveloperViewHolder, position: Int) {
|
||||
holder.binding.apply {
|
||||
val user = user[position]
|
||||
val score = user.score?.div(10.0) ?: 0.0
|
||||
setAnimation(root.context, root)
|
||||
profileUserName.text = user.name
|
||||
profileInfo.apply {
|
||||
text = when (user.status) {
|
||||
"CURRENT" -> "WATCHING"
|
||||
else -> user.status ?: ""
|
||||
}
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
profileCompactUserProgress.text = user.progress.toString()
|
||||
profileCompactScore.text = score.toString()
|
||||
profileCompactTotal.text = " | ${user.totalEpisodes ?: "~"}"
|
||||
profileUserAvatar.loadImage(user.pfp)
|
||||
|
||||
val scoreDrawable = if (score == 0.0) R.drawable.score else R.drawable.user_score
|
||||
profileCompactScoreBG.apply {
|
||||
visibility = View.VISIBLE
|
||||
background = ContextCompat.getDrawable(root.context, scoreDrawable)
|
||||
}
|
||||
|
||||
profileCompactProgressContainer.visibility = View.VISIBLE
|
||||
|
||||
profileUserAvatar.setOnClickListener {
|
||||
val intent = Intent(root.context, ProfileActivity::class.java).apply {
|
||||
putExtra("userId", user.id)
|
||||
}
|
||||
ContextCompat.startActivity(root.context, intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = user.size
|
||||
}
|
||||
56
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
56
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
interface Type {
|
||||
fun asText(): String
|
||||
}
|
||||
|
||||
enum class MediaType : Type {
|
||||
ANIME,
|
||||
MANGA,
|
||||
NOVEL;
|
||||
|
||||
override fun asText(): String {
|
||||
return when (this) {
|
||||
ANIME -> "Anime"
|
||||
MANGA -> "Manga"
|
||||
NOVEL -> "Novel"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string: String): MediaType? {
|
||||
return when (string) {
|
||||
"Anime" -> ANIME
|
||||
"Manga" -> MANGA
|
||||
"Novel" -> NOVEL
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class AddonType : Type {
|
||||
TORRENT,
|
||||
DOWNLOAD;
|
||||
|
||||
override fun asText(): String {
|
||||
return when (this) {
|
||||
TORRENT -> "Torrent"
|
||||
DOWNLOAD -> "Download"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string: String): AddonType? {
|
||||
return when (string) {
|
||||
"Torrent" -> TORRENT
|
||||
"Download" -> DOWNLOAD
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class OtherDetailsViewModel : ViewModel() {
|
||||
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
||||
suspend fun loadCalendar() {
|
||||
val curr = System.currentTimeMillis() / 1000
|
||||
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
|
||||
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
|
||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||
val map = mutableMapOf<String, MutableList<Media>>()
|
||||
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||
|
||||
@@ -27,7 +27,7 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
|
||||
return ProgressViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) {
|
||||
val progressBar = holder.binding.root
|
||||
bar = progressBar
|
||||
|
||||
@@ -4,24 +4,30 @@ import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
class SearchActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivitySearchBinding
|
||||
@@ -64,11 +70,18 @@ class SearchActivity : AppCompatActivity() {
|
||||
intent.getStringExtra("type") ?: "ANIME",
|
||||
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
|
||||
onList = listOnly,
|
||||
search = intent.getStringExtra("query"),
|
||||
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
|
||||
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
|
||||
sort = intent.getStringExtra("sortBy"),
|
||||
status = intent.getStringExtra("status"),
|
||||
source = intent.getStringExtra("source"),
|
||||
countryOfOrigin = intent.getStringExtra("country"),
|
||||
season = intent.getStringExtra("season"),
|
||||
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
|
||||
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")
|
||||
?.toIntOrNull() else null,
|
||||
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")
|
||||
?.toIntOrNull() else null,
|
||||
results = mutableListOf(),
|
||||
hasNextPage = false
|
||||
)
|
||||
@@ -127,8 +140,12 @@ class SearchActivity : AppCompatActivity() {
|
||||
excludedTags = it.excludedTags
|
||||
tags = it.tags
|
||||
season = it.season
|
||||
startYear = it.startYear
|
||||
seasonYear = it.seasonYear
|
||||
status = it.status
|
||||
source = it.source
|
||||
format = it.format
|
||||
countryOfOrigin = it.countryOfOrigin
|
||||
page = it.page
|
||||
hasNextPage = it.hasNextPage
|
||||
}
|
||||
@@ -137,7 +154,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
model.searchResults.results.addAll(it.results)
|
||||
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||
|
||||
progressAdapter.bar?.visibility = if (it.hasNextPage) View.VISIBLE else View.GONE
|
||||
progressAdapter.bar?.isVisible = it.hasNextPage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +168,10 @@ class SearchActivity : AppCompatActivity() {
|
||||
} else
|
||||
headerAdaptor.requestFocus?.run()
|
||||
|
||||
if (intent.getBooleanExtra("search", false)) search()
|
||||
if (intent.getBooleanExtra("search", false)) {
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED)
|
||||
search()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.PopupMenu
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||
@@ -28,7 +28,9 @@ import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.imagesearch.ImageSearchActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.android.material.checkbox.MaterialCheckBox.*
|
||||
import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED
|
||||
import com.google.android.material.checkbox.MaterialCheckBox.STATE_INDETERMINATE
|
||||
import com.google.android.material.checkbox.MaterialCheckBox.STATE_UNCHECKED
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -44,6 +46,20 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
||||
private lateinit var 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 {
|
||||
val binding =
|
||||
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
@@ -92,7 +108,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
binding.searchAdultCheck.isChecked = adult
|
||||
binding.searchList.isChecked = listOnly == true
|
||||
|
||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
||||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
|
||||
activity.updateChips = { it.update() }
|
||||
}
|
||||
|
||||
@@ -102,6 +118,65 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
binding.searchFilter.setOnClickListener {
|
||||
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))
|
||||
}
|
||||
@@ -230,14 +305,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
private fun fadeInAnimation(): Animation {
|
||||
return AlphaAnimation(0f, 1f).apply {
|
||||
duration = 150
|
||||
fillAfter = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun fadeOutAnimation(): Animation {
|
||||
return AlphaAnimation(1f, 0f).apply {
|
||||
duration = 150
|
||||
fillAfter = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +329,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
}
|
||||
|
||||
|
||||
class SearchChipAdapter(val activity: SearchActivity) :
|
||||
class SearchChipAdapter(
|
||||
val activity: SearchActivity,
|
||||
private val searchAdapter: SearchAdapter
|
||||
) :
|
||||
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||
private var chips = activity.result.toChipList()
|
||||
|
||||
@@ -273,11 +349,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
|
||||
val chip = chips[position]
|
||||
holder.binding.root.apply {
|
||||
text = chip.text
|
||||
text = chip.text.replace("_", " ")
|
||||
setOnClickListener {
|
||||
activity.result.removeChip(chip)
|
||||
update()
|
||||
activity.search()
|
||||
searchAdapter.updateFilterTextViewDrawable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,6 +363,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
fun update() {
|
||||
chips = activity.result.toChipList()
|
||||
notifyDataSetChanged()
|
||||
searchAdapter.updateFilterTextViewDrawable()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = chips.size
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.PopupMenu
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -17,6 +21,9 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
|
||||
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
@@ -38,6 +45,54 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
private var exGenres = mutableListOf<String>()
|
||||
private var selectedTags = 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?) {
|
||||
|
||||
@@ -47,14 +102,157 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
exGenres = activity.result.excludedGenres ?: mutableListOf()
|
||||
selectedTags = activity.result.tags ?: 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 {
|
||||
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 }
|
||||
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 }
|
||||
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
if (activity.result.type == "ANIME") {
|
||||
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
} else {
|
||||
startYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
}
|
||||
sort = activity.result.sort
|
||||
countryOfOrigin = activity.result.countryOfOrigin
|
||||
genres = selectedGenres
|
||||
tags = selectedTags
|
||||
excludedGenres = exGenres
|
||||
@@ -67,15 +265,23 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
binding.searchFilterCancel.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.searchSortBy.setText(activity.result.sort?.let {
|
||||
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
|
||||
})
|
||||
binding.searchSortBy.setAdapter(
|
||||
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,
|
||||
resources.getStringArray(R.array.sort_by)
|
||||
format
|
||||
)
|
||||
)
|
||||
|
||||
binding.searchSource.setText(activity.result.source?.replace("_", " "))
|
||||
binding.searchSource.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
Anilist.source.toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -84,11 +290,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
|
||||
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
|
||||
if (activity.result.type == "ANIME") {
|
||||
binding.searchYear.setText(activity.result.seasonYear?.toString())
|
||||
} else {
|
||||
binding.searchYear.setText(activity.result.startYear?.toString())
|
||||
}
|
||||
binding.searchYear.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
|
||||
.reversed().toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
|
||||
else {
|
||||
binding.searchSeason.setText(activity.result.season)
|
||||
binding.searchSeason.setAdapter(
|
||||
@@ -98,16 +318,6 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
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 ->
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -22,7 +21,6 @@ abstract class SourceAdapter(
|
||||
return SourceViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val character = sources[position]
|
||||
|
||||
@@ -65,7 +65,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
i = media!!.selected!!.sourceIndex
|
||||
|
||||
val source = if (media!!.anime != null) {
|
||||
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
|
||||
(if (media!!.isAdult) HAnimeSources else AnimeSources)[i!!]
|
||||
} else {
|
||||
anime = false
|
||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -114,7 +115,7 @@ class StudioActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.studioProgressBar.isGone = loaded
|
||||
super.onResume()
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,19 @@ import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.snackString
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class SubtitleDownloader {
|
||||
|
||||
companion object {
|
||||
//doesn't really download the subtitles -\_(o_o)_/-
|
||||
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
|
||||
suspend fun loadSubtitleType(url: String): SubtitleType =
|
||||
withContext(Dispatchers.IO) {
|
||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
@@ -51,21 +51,17 @@ class SubtitleDownloader {
|
||||
downloadedType: DownloadedType
|
||||
) {
|
||||
try {
|
||||
val directory = DownloadsManager.getDirectory(
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context,
|
||||
downloadedType.type,
|
||||
downloadedType.title,
|
||||
downloadedType.chapter
|
||||
)
|
||||
if (!directory.exists()) { //just in case
|
||||
directory.mkdirs()
|
||||
}
|
||||
val type = loadSubtitleType(context, url)
|
||||
val subtiteFile = File(directory, "subtitle.${type}")
|
||||
if (subtiteFile.exists()) {
|
||||
subtiteFile.delete()
|
||||
}
|
||||
subtiteFile.createNewFile()
|
||||
false,
|
||||
downloadedType.titleName,
|
||||
downloadedType.chapterName
|
||||
) ?: throw Exception("Could not create directory")
|
||||
val type = loadSubtitleType(url)
|
||||
directory.findFile("subtitle.${type}")?.delete()
|
||||
val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
|
||||
?: throw Exception("Could not create subtitle file")
|
||||
|
||||
val client = Injekt.get<NetworkHelper>().client
|
||||
val request = Request.Builder().url(url).build()
|
||||
@@ -77,7 +73,8 @@ class SubtitleDownloader {
|
||||
}
|
||||
|
||||
reponse.body.byteStream().use { input ->
|
||||
subtiteFile.outputStream().use { output ->
|
||||
subtitleFile.openOutputStream(context, false).use { output ->
|
||||
if (output == null) throw Exception("Could not open output stream")
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.navBarHeight
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
|
||||
class TripleNavAdapter(
|
||||
private val nav1: AnimatedBottomBar,
|
||||
private val nav2: AnimatedBottomBar,
|
||||
private val nav3: AnimatedBottomBar,
|
||||
anime: Boolean,
|
||||
format: String,
|
||||
private val isScreenVertical: Boolean = false
|
||||
) {
|
||||
var selected: Int = 0
|
||||
var selectionListener: ((Int, Int) -> Unit)? = null
|
||||
init {
|
||||
nav1.tabs.clear()
|
||||
nav2.tabs.clear()
|
||||
nav3.tabs.clear()
|
||||
val infoTab = nav1.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
|
||||
val watchTab = if (anime) {
|
||||
nav2.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
|
||||
} else if (format == "NOVEL") {
|
||||
nav2.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
|
||||
} else {
|
||||
nav2.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
|
||||
}
|
||||
val commentTab = nav3.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
|
||||
nav1.addTab(infoTab)
|
||||
nav1.visibility = ViewGroup.VISIBLE
|
||||
if (isScreenVertical) {
|
||||
nav2.visibility = ViewGroup.GONE
|
||||
nav3.visibility = ViewGroup.GONE
|
||||
nav1.addTab(watchTab)
|
||||
nav1.addTab(commentTab)
|
||||
nav1.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
nav2.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
nav3.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
} else {
|
||||
nav1.indicatorColor = Color.TRANSPARENT
|
||||
nav2.indicatorColor = Color.TRANSPARENT
|
||||
nav3.indicatorColor = Color.TRANSPARENT
|
||||
nav2.visibility = ViewGroup.VISIBLE
|
||||
nav3.visibility = ViewGroup.VISIBLE
|
||||
nav2.addTab(watchTab)
|
||||
nav3.addTab(commentTab)
|
||||
nav2.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
selected = 1
|
||||
deselectOthers(selected)
|
||||
selectionListener?.invoke(selected, newTab.id)
|
||||
}
|
||||
})
|
||||
nav3.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
selected = 2
|
||||
deselectOthers(selected)
|
||||
selectionListener?.invoke(selected, newTab.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
nav1.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
if (!isScreenVertical) {
|
||||
selected = 0
|
||||
deselectOthers(selected)
|
||||
} else selected = newIndex
|
||||
selectionListener?.invoke(selected, newTab.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun deselectOthers(selected: Int) {
|
||||
if (selected == 0) {
|
||||
nav2.clearSelection()
|
||||
nav3.clearSelection()
|
||||
}
|
||||
if (selected == 1) {
|
||||
nav1.clearSelection()
|
||||
nav3.clearSelection()
|
||||
}
|
||||
if (selected == 2) {
|
||||
nav1.clearSelection()
|
||||
nav2.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectTab(tab: Int) {
|
||||
selected = tab
|
||||
if (!isScreenVertical) {
|
||||
when (tab) {
|
||||
0 -> nav1.selectTabAt(0)
|
||||
1 -> nav2.selectTabAt(0)
|
||||
2 -> nav3.selectTabAt(0)
|
||||
}
|
||||
deselectOthers(selected)
|
||||
} else {
|
||||
nav1.selectTabAt(selected)
|
||||
}
|
||||
}
|
||||
|
||||
fun setVisibility(visibility: Int) {
|
||||
if (isScreenVertical) {
|
||||
nav1.visibility = visibility
|
||||
return
|
||||
}
|
||||
nav1.visibility = visibility
|
||||
nav2.visibility = visibility
|
||||
nav3.visibility = visibility
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import java.util.Locale
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class AnimeNameAdapter {
|
||||
companion object {
|
||||
const val episodeRegex =
|
||||
"(episode|episodio|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
const val failedEpisodeNumberRegex =
|
||||
"(?<!part\\s)\\b(\\d+)\\b"
|
||||
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
||||
|
||||
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
||||
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val soft = subdubMatcher.group(1)
|
||||
val subdub = subdubMatcher.group(2)
|
||||
val bed = subdubMatcher.group(3) ?: ""
|
||||
|
||||
val toggled = when (typeToSetTo) {
|
||||
SubDubType.SUB -> "sub"
|
||||
SubDubType.DUB -> "dub"
|
||||
SubDubType.NULL -> ""
|
||||
}
|
||||
val toggledCasePreserved =
|
||||
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
|
||||
?.isUpperCase() == true
|
||||
) toggled.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(
|
||||
Locale.ROOT
|
||||
) else it.toString()
|
||||
} else toggled
|
||||
|
||||
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubDub(text: String): SubDubType {
|
||||
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
|
||||
when (subdub) {
|
||||
"sub" -> SubDubType.SUB
|
||||
"dub" -> SubDubType.DUB
|
||||
else -> SubDubType.NULL
|
||||
}
|
||||
} else {
|
||||
SubDubType.NULL
|
||||
}
|
||||
}
|
||||
|
||||
enum class SubDubType {
|
||||
SUB, DUB, NULL
|
||||
}
|
||||
|
||||
fun findSeasonNumber(text: String): Int? {
|
||||
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
|
||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||
|
||||
return if (seasonMatcher.find()) {
|
||||
seasonMatcher.group(2)?.toInt()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun findEpisodeNumber(text: String): Float? {
|
||||
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
|
||||
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||
|
||||
return if (episodeMatcher.find()) {
|
||||
if (episodeMatcher.group(2) != null) {
|
||||
episodeMatcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedEpisodeNumberPattern: Pattern =
|
||||
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
|
||||
val failedEpisodeNumberMatcher: Matcher =
|
||||
failedEpisodeNumberPattern.matcher(text)
|
||||
if (failedEpisodeNumberMatcher.find()) {
|
||||
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEpisodeNumber(text: String): String {
|
||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||
text
|
||||
}
|
||||
val letterPattern = Regex("[a-zA-Z]")
|
||||
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||
removedNumber
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeEpisodeNumberCompletely(text: String): String {
|
||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "")
|
||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||
val failedEpisodeNumberPattern: Regex =
|
||||
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||
}
|
||||
} else {
|
||||
removedNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import ani.dantotsu.settings.FAQActivity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageButton
|
||||
@@ -13,22 +11,34 @@ import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
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.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.openSettings
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.others.webview.CookieCatcher
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.FAQActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
|
||||
@@ -42,7 +52,7 @@ class AnimeWatchAdapter(
|
||||
private val fragment: AnimeWatchFragment,
|
||||
private val watchSources: WatchSources
|
||||
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
|
||||
|
||||
private var autoSelect = true
|
||||
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
||||
private var _binding: ItemAnimeWatchBinding? = null
|
||||
|
||||
@@ -54,7 +64,6 @@ class AnimeWatchAdapter(
|
||||
private var nestedDialog: AlertDialog? = null
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
_binding = binding
|
||||
@@ -97,15 +106,12 @@ class AnimeWatchAdapter(
|
||||
null
|
||||
)
|
||||
}
|
||||
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal(
|
||||
PrefName.OfflineMode
|
||||
)
|
||||
) View.GONE else View.VISIBLE
|
||||
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
||||
|
||||
binding.animeSourceNameContainer.visibility = offline
|
||||
binding.animeSourceSettings.visibility = offline
|
||||
binding.animeSourceSearch.visibility = offline
|
||||
binding.animeSourceTitle.visibility = offline
|
||||
binding.animeSourceNameContainer.isGone = offline
|
||||
binding.animeSourceSettings.isGone = offline
|
||||
binding.animeSourceSearch.isGone = offline
|
||||
binding.animeSourceTitle.isGone = offline
|
||||
|
||||
//Source Selection
|
||||
var source =
|
||||
@@ -117,8 +123,7 @@ class AnimeWatchAdapter(
|
||||
this.selectDub = media.selected!!.preferDub
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,8 +142,7 @@ class AnimeWatchAdapter(
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||
source = i
|
||||
setLanguageList(0, i)
|
||||
}
|
||||
@@ -158,14 +162,12 @@ class AnimeWatchAdapter(
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility =
|
||||
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
|
||||
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||
setLanguageList(i, source)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
} ?: run {
|
||||
}
|
||||
} ?: run { }
|
||||
}
|
||||
|
||||
//settings
|
||||
@@ -223,9 +225,9 @@ class AnimeWatchAdapter(
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> dialogBinding.layoutText.text = "List"
|
||||
1 -> dialogBinding.layoutText.text = "Grid"
|
||||
2 -> dialogBinding.layoutText.text = "Compact"
|
||||
0 -> dialogBinding.layoutText.setText(R.string.list)
|
||||
1 -> dialogBinding.layoutText.setText(R.string.grid)
|
||||
2 -> dialogBinding.layoutText.setText(R.string.compact)
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
@@ -237,24 +239,24 @@ class AnimeWatchAdapter(
|
||||
dialogBinding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 0
|
||||
dialogBinding.layoutText.text = "List"
|
||||
dialogBinding.layoutText.setText(R.string.list)
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceGrid.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 1
|
||||
dialogBinding.layoutText.text = "Grid"
|
||||
dialogBinding.layoutText.setText(R.string.grid)
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeSourceCompact.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 2
|
||||
dialogBinding.layoutText.text = "Compact"
|
||||
dialogBinding.layoutText.setText(R.string.compact)
|
||||
run = true
|
||||
}
|
||||
dialogBinding.animeWebviewContainer.setOnClickListener {
|
||||
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||
toast("WebView not installed")
|
||||
toast(R.string.webview_not_installed)
|
||||
}
|
||||
//start CookieCatcher activity
|
||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||
@@ -307,7 +309,6 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
|
||||
//Chips
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
|
||||
val binding = _binding
|
||||
if (binding != null) {
|
||||
@@ -329,7 +330,9 @@ class AnimeWatchAdapter(
|
||||
0
|
||||
)
|
||||
}
|
||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
|
||||
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
chip.text = chipText
|
||||
chip.setTextColor(
|
||||
ContextCompat.getColorStateList(
|
||||
fragment.requireContext(),
|
||||
@@ -363,7 +366,6 @@ class AnimeWatchAdapter(
|
||||
_binding?.animeSourceChipGroup?.removeAllViews()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun handleEpisodes() {
|
||||
val binding = _binding
|
||||
if (binding != null) {
|
||||
@@ -371,9 +373,9 @@ class AnimeWatchAdapter(
|
||||
val episodes = media.anime.episodes!!.keys.toTypedArray()
|
||||
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp =
|
||||
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
|
||||
?: 1
|
||||
val appEp = PrefManager.getCustomVal<String?>(
|
||||
"${media.id}_current_ep", ""
|
||||
)?.toIntOrNull() ?: 1
|
||||
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
if (episodes.contains(continueEp)) {
|
||||
@@ -403,21 +405,27 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
val ep = media.anime.episodes!![continueEp]!!
|
||||
|
||||
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
|
||||
|
||||
binding.itemEpisodeImage.loadImage(
|
||||
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
||||
)
|
||||
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeSourceContinueText.text =
|
||||
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
|
||||
currActivity()!!.getString(
|
||||
R.string.continue_episode, ep.number, if (ep.filler)
|
||||
currActivity()!!.getString(R.string.filler_tag)
|
||||
else
|
||||
"", cleanedTitle
|
||||
)
|
||||
binding.animeSourceContinue.setOnClickListener {
|
||||
fragment.onEpisodeClick(continueEp)
|
||||
}
|
||||
if (fragment.continueEp) {
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
|
||||
PrefName.WatchPercentage
|
||||
)
|
||||
if (
|
||||
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
|
||||
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
|
||||
) {
|
||||
binding.animeSourceContinue.performClick()
|
||||
fragment.continueEp = false
|
||||
@@ -428,13 +436,31 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
|
||||
binding.animeSourceProgressBar.visibility = View.GONE
|
||||
if (media.anime.episodes!!.isNotEmpty()) {
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
binding.faqbutton.visibility = View.GONE}
|
||||
else {
|
||||
binding.animeSourceNotFound.visibility = View.VISIBLE
|
||||
binding.faqbutton.visibility = View.VISIBLE
|
||||
|
||||
val sourceFound = media.anime.episodes!!.isNotEmpty()
|
||||
binding.animeSourceNotFound.isGone = sourceFound
|
||||
binding.faqbutton.isGone = sourceFound
|
||||
|
||||
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 {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
@@ -480,8 +506,7 @@ class AnimeWatchAdapter(
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
//Timer
|
||||
countDown(media, binding.animeSourceContainer)
|
||||
displayTimer(media, binding.animeSourceContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,34 +17,45 @@ import androidx.annotation.OptIn
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
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.parsers.AnimeParser
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.setNavigationTheme
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
@@ -188,10 +199,16 @@ class AnimeWatchFragment : Fragment() {
|
||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
awaitAll(
|
||||
async { model.loadKitsuEpisodes(media) },
|
||||
async { model.loadFillerEpisodes(media) }
|
||||
)
|
||||
val offline =
|
||||
!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
||||
if (offline) {
|
||||
media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex
|
||||
} else {
|
||||
awaitAll(
|
||||
async { model.loadKitsuEpisodes(media) },
|
||||
async { model.loadFillerEpisodes(media) }
|
||||
)
|
||||
}
|
||||
model.loadEpisodes(media, media.selected!!.sourceIndex)
|
||||
}
|
||||
loaded = true
|
||||
@@ -216,7 +233,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||
episode.desc =
|
||||
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
|
||||
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
|
||||
episode.title ?: ""
|
||||
).isBlank()
|
||||
) media.anime!!.kitsuEpisodes!![i]?.title
|
||||
@@ -340,16 +357,12 @@ class AnimeWatchFragment : Fragment() {
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = activity
|
||||
if (activity is MediaDetailsActivity && isAdded) {
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||
|
||||
activity.tabLayout.setVisibility(visibility)
|
||||
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).isVisible = show
|
||||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).isVisible = show
|
||||
activity.findViewById<CardView>(R.id.mediaCover).isVisible = show
|
||||
activity.findViewById<CardView>(R.id.mediaClose).isVisible = show
|
||||
activity.navBar.isVisible = show
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).isGone = show
|
||||
}
|
||||
}
|
||||
var itemSelected = false
|
||||
@@ -417,7 +430,29 @@ class AnimeWatchFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||
activity?.let {
|
||||
if (!hasDirAccess(it)) {
|
||||
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
|
||||
if (success) {
|
||||
model.onEpisodeClick(
|
||||
media,
|
||||
i,
|
||||
requireActivity().supportFragmentManager,
|
||||
isDownload = true
|
||||
)
|
||||
} else {
|
||||
snackString(getString(R.string.download_permission_required))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
model.onEpisodeClick(
|
||||
media,
|
||||
i,
|
||||
requireActivity().supportFragmentManager,
|
||||
isDownload = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
||||
@@ -435,10 +470,11 @@ class AnimeWatchFragment : Fragment() {
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
DownloadedType.Type.ANIME
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
episodeAdapter.purgeDownload(i)
|
||||
) {
|
||||
episodeAdapter.purgeDownload(i)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -447,22 +483,13 @@ class AnimeWatchFragment : Fragment() {
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
DownloadedType.Type.ANIME
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
||||
DownloadService.sendRemoveDownload(
|
||||
requireContext(),
|
||||
ExoplayerDownloadService::class.java,
|
||||
id,
|
||||
true
|
||||
)
|
||||
episodeAdapter.deleteDownload(i)
|
||||
) {
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
||||
episodeAdapter.deleteDownload(i)
|
||||
}
|
||||
}
|
||||
|
||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||
@@ -526,8 +553,8 @@ class AnimeWatchFragment : Fragment() {
|
||||
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
for (download in downloadManager.animeDownloadedTypes) {
|
||||
if (download.title == media.mainName()) {
|
||||
episodeAdapter.stopDownload(download.chapter)
|
||||
if (media.compareName(download.titleName)) {
|
||||
episodeAdapter.stopDownload(download.chapterName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -8,18 +7,21 @@ import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadIndex
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getDirSize
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
@@ -53,15 +55,7 @@ class EpisodeAdapter(
|
||||
var arr: List<Episode> = arrayListOf(),
|
||||
var offlineMode: Boolean
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private lateinit var index: DownloadIndex
|
||||
|
||||
|
||||
init {
|
||||
if (offlineMode) {
|
||||
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
|
||||
}
|
||||
}
|
||||
val context = fragment.requireContext()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return (when (viewType) {
|
||||
@@ -97,11 +91,10 @@ class EpisodeAdapter(
|
||||
return type
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val ep = arr[position]
|
||||
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
|
||||
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
|
||||
ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
|
||||
} else {
|
||||
ep.number
|
||||
} ?: ""
|
||||
@@ -125,8 +118,7 @@ class EpisodeAdapter(
|
||||
binding.itemEpisodeFiller.visibility = View.GONE
|
||||
binding.itemEpisodeFillerView.visibility = View.GONE
|
||||
}
|
||||
binding.itemEpisodeDesc.visibility =
|
||||
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
|
||||
binding.itemEpisodeDesc.isVisible = !ep.desc.isNullOrBlank()
|
||||
binding.itemEpisodeDesc.text = ep.desc ?: ""
|
||||
holder.bind(ep.number, ep.downloadProgress, ep.desc)
|
||||
|
||||
@@ -203,8 +195,7 @@ class EpisodeAdapter(
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeFillerView.visibility =
|
||||
if (ep.filler) View.VISIBLE else View.GONE
|
||||
binding.itemEpisodeFillerView.isVisible = ep.filler
|
||||
if (media.userProgress != null) {
|
||||
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
|
||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||
@@ -248,17 +239,8 @@ class EpisodeAdapter(
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||
if (position != -1) {
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
|
||||
media.mainName(),
|
||||
episodeNumber
|
||||
)
|
||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
val size = try {
|
||||
val download = index.getDownload(id)
|
||||
bytesToHuman(download?.bytesDownloaded ?: 0)
|
||||
bytesToHuman(getDirSize(context, MediaType.ANIME, media.mainName(), episodeNumber))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@@ -429,7 +411,7 @@ class EpisodeAdapter(
|
||||
if (bytes < 0) return null
|
||||
val unit = 1000
|
||||
if (bytes < unit) return "$bytes B"
|
||||
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||
val pre = ("KMGTPE")[exp - 1]
|
||||
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@ package ani.dantotsu.media.anime
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
@@ -12,29 +14,51 @@ import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||
import ani.dantotsu.databinding.ItemStreamBinding
|
||||
import ani.dantotsu.databinding.ItemUrlBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.hideSystemBars
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.Download.download
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWith
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DecimalFormat
|
||||
@@ -211,10 +235,101 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
private val externalPlayerResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result: ActivityResult ->
|
||||
Logger.log(result.data.toString())
|
||||
}
|
||||
|
||||
private fun exportMagnetIntent(episode: Episode, video: Video): Intent {
|
||||
val amnis = "com.amnis"
|
||||
return Intent(Intent.ACTION_VIEW).apply {
|
||||
component = ComponentName(amnis, "$amnis.gui.player.PlayerActivity")
|
||||
data = Uri.parse(video.file.url)
|
||||
putExtra("title", "${media?.name} - ${episode.title}")
|
||||
putExtra("position", 0)
|
||||
putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
putExtra("secure_uri", true)
|
||||
val headersArray = arrayOf<String>()
|
||||
video.file.headers.forEach {
|
||||
headersArray.plus(arrayOf(it.key, it.value))
|
||||
}
|
||||
putExtra("headers", headersArray)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun startExoplayer(media: Media) {
|
||||
prevEpisode = null
|
||||
|
||||
episode?.let { ep ->
|
||||
val video = ep.extractors?.find {
|
||||
it.server.name == ep.selectedExtractor
|
||||
}?.videos?.getOrNull(ep.selectedVideo)
|
||||
video?.file?.url?.let { url ->
|
||||
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
||||
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||
if (torrentExtension.isAvailable()) {
|
||||
val activity = currActivity() ?: requireActivity()
|
||||
launchIO {
|
||||
val extension = torrentExtension.extension!!.extension
|
||||
torrentExtension.torrentHash?.let {
|
||||
extension.removeTorrent(it)
|
||||
}
|
||||
val index = if (url.contains("index=")) {
|
||||
url.substringAfter("index=").toIntOrNull() ?: 0
|
||||
} else 0
|
||||
Logger.log("Sending: ${url}, ${video.quality}, $index")
|
||||
val currentTorrent = extension.addTorrent(
|
||||
url, video.quality.toString(), "", "", false
|
||||
)
|
||||
torrentExtension.torrentHash = currentTorrent.hash
|
||||
video.file.url = extension.getLink(currentTorrent, index)
|
||||
Logger.log("Received: ${video.file.url}")
|
||||
if (launch == true) {
|
||||
Intent(activity, ExoplayerView::class.java).apply {
|
||||
ExoplayerView.media = media
|
||||
ExoplayerView.initialized = true
|
||||
startActivity(this)
|
||||
}
|
||||
} else {
|
||||
model.setEpisode(
|
||||
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
|
||||
"startExo no launch"
|
||||
)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
externalPlayerResult.launch(exportMagnetIntent(ep, video))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val amnis = "com.amnis"
|
||||
try {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=$amnis")
|
||||
)
|
||||
)
|
||||
dismiss()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
||||
stopAddingToList()
|
||||
@@ -302,7 +417,6 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val video = extractor.videos[position]
|
||||
@@ -311,6 +425,49 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
} else {
|
||||
binding.urlDownload.visibility = View.GONE
|
||||
}
|
||||
val subtitles = extractor.subtitles
|
||||
if (subtitles.isNotEmpty()) {
|
||||
binding.urlSub.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.urlSub.visibility = View.GONE
|
||||
}
|
||||
binding.urlSub.setOnClickListener {
|
||||
if (subtitles.isNotEmpty()) {
|
||||
val subtitleNames = subtitles.map { it.language }
|
||||
var subtitleToDownload: Subtitle? = null
|
||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Subtitle")
|
||||
.setSingleChoiceItems(
|
||||
subtitleNames.toTypedArray(),
|
||||
-1
|
||||
) { _, which ->
|
||||
subtitleToDownload = subtitles[which]
|
||||
}
|
||||
.setPositiveButton("Download") { dialog, _ ->
|
||||
scope.launch {
|
||||
if (subtitleToDownload != null) {
|
||||
SubtitleDownloader.downloadSubtitle(
|
||||
requireContext(),
|
||||
subtitleToDownload!!.file.url,
|
||||
DownloadedType(
|
||||
media!!.mainName(),
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number,
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
} else {
|
||||
snackString("No Subtitles Available")
|
||||
}
|
||||
}
|
||||
binding.urlDownload.setSafeOnClickListener {
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||
extractor.server.name
|
||||
@@ -323,20 +480,52 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
media!!.userPreferredName
|
||||
)
|
||||
} else {
|
||||
val downloadAddonManager: DownloadAddonManager = Injekt.get()
|
||||
if (!downloadAddonManager.isAvailable()){
|
||||
toast("Download Extension not available")
|
||||
return@setSafeOnClickListener
|
||||
}
|
||||
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
||||
val selectedVideo =
|
||||
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||
val subtitles = extractor.subtitles
|
||||
val subtitleNames = subtitles.map { it.language }
|
||||
var subtitleToDownload: Subtitle? = null
|
||||
val activity = currActivity()?:requireActivity()
|
||||
val activity = currActivity() ?: requireActivity()
|
||||
selectedVideo?.file?.url?.let { url ->
|
||||
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
||||
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||
if (!torrentExtension.isAvailable()) {
|
||||
toast("Torrent Extension not available")
|
||||
return@setSafeOnClickListener
|
||||
}
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
val extension = torrentExtension.extension!!.extension
|
||||
torrentExtension.torrentHash?.let {
|
||||
extension.removeTorrent(it)
|
||||
}
|
||||
val index = if (url.contains("index=")) {
|
||||
url.substringAfter("index=").toIntOrNull() ?: 0
|
||||
} else 0
|
||||
Logger.log("Sending: ${url}, ${selectedVideo.quality}, $index")
|
||||
val currentTorrent = extension.addTorrent(
|
||||
url, selectedVideo.quality.toString(), "", "", false
|
||||
)
|
||||
torrentExtension.torrentHash = currentTorrent.hash
|
||||
selectedVideo.file.url =
|
||||
extension.getLink(currentTorrent, index)
|
||||
Logger.log("Received: ${selectedVideo.file.url}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (subtitles.isNotEmpty()) {
|
||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Subtitle")
|
||||
.setSingleChoiceItems(
|
||||
subtitleNames.toTypedArray(),
|
||||
-1
|
||||
) { dialog, which ->
|
||||
) { _, which ->
|
||||
subtitleToDownload = subtitles[which]
|
||||
}
|
||||
.setPositiveButton("Download") { _, _ ->
|
||||
@@ -401,12 +590,16 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
if (video.format == VideoType.CONTAINER) {
|
||||
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
|
||||
binding.urlSize.text =
|
||||
// if video size is null or 0, show "Unknown Size" else show the size in MB
|
||||
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
|
||||
"#.##"
|
||||
).format(video.size ?: 0).toString() + " MB"))
|
||||
binding.urlSize.isVisible = video.size != null
|
||||
// if video size is null or 0, show "Unknown Size" else show the size in MB
|
||||
val sizeText = getString(
|
||||
R.string.mb_size, "${if (video.extraNote != null) " : " else ""}${
|
||||
if (video.size == 0.0) getString(R.string.size_unknown) else DecimalFormat("#.##").format(
|
||||
video.size ?: 0
|
||||
)
|
||||
}"
|
||||
)
|
||||
binding.urlSize.text = sizeText
|
||||
}
|
||||
binding.urlNote.visibility = View.VISIBLE
|
||||
binding.urlNote.text = video.format.name
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Color.TRANSPARENT
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -68,7 +67,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.subtitleTitle.setText(R.string.none)
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
val selSubs = PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java)
|
||||
val selSubs = PrefManager.getNullableCustomVal(
|
||||
"subLang_${mediaID}",
|
||||
null,
|
||||
String::class.java
|
||||
)
|
||||
if (episode.selectedSubtitle != null && selSubs != "None") {
|
||||
binding.root.setCardBackgroundColor(TRANSPARENT)
|
||||
}
|
||||
@@ -108,12 +111,15 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
val selSubs: String? =
|
||||
PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java)
|
||||
PrefManager.getNullableCustomVal(
|
||||
"subLang_${mediaID}",
|
||||
null,
|
||||
String::class.java
|
||||
)
|
||||
if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) {
|
||||
binding.root.setCardBackgroundColor(TRANSPARENT)
|
||||
}
|
||||
}
|
||||
val activity: Activity = requireActivity() as ExoplayerView
|
||||
binding.root.setOnClickListener {
|
||||
episode.selectedSubtitle = position - 1
|
||||
model.setEpisode(episode, "Subtitle")
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||
import androidx.media3.common.C.TrackType
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.BottomSheetSubtitlesBinding
|
||||
import ani.dantotsu.databinding.ItemSubtitleTextBinding
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class TrackGroupDialogFragment(
|
||||
instance: ExoplayerView, trackGroups: ArrayList<Tracks.Group>, type: @TrackType Int
|
||||
) : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSubtitlesBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var instance: ExoplayerView
|
||||
private var trackGroups: ArrayList<Tracks.Group>
|
||||
private var type: @TrackType Int
|
||||
|
||||
init {
|
||||
this.instance = instance
|
||||
this.trackGroups = trackGroups
|
||||
this.type = type
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (type == TRACK_TYPE_AUDIO) binding.selectionTitle.text = getString(R.string.audio_tracks)
|
||||
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.subtitlesRecycler.adapter = TrackGroupAdapter()
|
||||
}
|
||||
|
||||
inner class TrackGroupAdapter : RecyclerView.Adapter<TrackGroupAdapter.StreamViewHolder>() {
|
||||
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
||||
StreamViewHolder(
|
||||
ItemSubtitleTextBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
trackGroups[position].let { trackGroup ->
|
||||
when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) {
|
||||
null -> {
|
||||
binding.subtitleTitle.text =
|
||||
getString(R.string.unknown_track, "Track $position")
|
||||
}
|
||||
|
||||
"none" -> {
|
||||
binding.subtitleTitle.text = getString(R.string.disabled_track)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val locale = if (language.contains("-")) {
|
||||
val parts = language.split("-")
|
||||
try {
|
||||
Locale(parts[0], parts[1])
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Locale(language)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
binding.subtitleTitle.text = locale?.let {
|
||||
"[${it.language}] ${it.displayName}"
|
||||
|
||||
} ?: getString(R.string.unknown_track, language)
|
||||
}
|
||||
}
|
||||
if (trackGroup.isSelected) {
|
||||
val selected = "✔ ${binding.subtitleTitle.text}"
|
||||
binding.subtitleTitle.text = selected
|
||||
}
|
||||
binding.root.setOnClickListener {
|
||||
dismiss()
|
||||
instance.onSetTrackGroupOverride(trackGroup, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = trackGroups.size
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
@@ -11,6 +12,7 @@ import ani.dantotsu.connections.comments.Comment
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ItemCommentsBinding
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
@@ -28,17 +30,20 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class CommentItem(val comment: Comment,
|
||||
private val markwon: Markwon,
|
||||
val parentSection: Section,
|
||||
private val commentsFragment: CommentsFragment,
|
||||
private val backgroundColor: Int,
|
||||
val commentDepth: Int
|
||||
) : BindableItem<ItemCommentsBinding>() {
|
||||
class CommentItem(
|
||||
val comment: Comment,
|
||||
private val markwon: Markwon,
|
||||
val parentSection: Section,
|
||||
private val commentsFragment: CommentsFragment,
|
||||
private val backgroundColor: Int,
|
||||
val commentDepth: Int
|
||||
) :
|
||||
BindableItem<ItemCommentsBinding>() {
|
||||
lateinit var binding: ItemCommentsBinding
|
||||
val adapter = GroupieAdapter()
|
||||
private var subCommentIds: MutableList<Int> = mutableListOf()
|
||||
@@ -52,18 +57,15 @@ class CommentItem(val comment: Comment,
|
||||
adapter.add(repliesSection)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
|
||||
binding = viewBinding
|
||||
setAnimation(binding.root.context, binding.root)
|
||||
viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(commentsFragment.activity)
|
||||
viewBinding.commentRepliesList.layoutManager =
|
||||
LinearLayoutManager(commentsFragment.activity)
|
||||
viewBinding.commentRepliesList.adapter = adapter
|
||||
val isUserComment = CommentsAPI.userId == comment.userId
|
||||
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
|
||||
markwon.setMarkdown(viewBinding.commentText, comment.content)
|
||||
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
|
||||
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
|
||||
viewBinding.commentReport.visibility = if (!isUserComment) View.VISIBLE else View.GONE
|
||||
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
||||
if (comment.tag == null) {
|
||||
viewBinding.commentUserTagLayout.visibility = View.GONE
|
||||
@@ -76,8 +78,15 @@ class CommentItem(val comment: Comment,
|
||||
if ((comment.replyCount ?: 0) > 0) {
|
||||
viewBinding.commentTotalReplies.visibility = View.VISIBLE
|
||||
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
|
||||
viewBinding.commentTotalReplies.text = if(repliesVisible) "Hide Replies" else
|
||||
"View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
|
||||
viewBinding.commentTotalReplies.context.run {
|
||||
viewBinding.commentTotalReplies.text = if (repliesVisible)
|
||||
getString(R.string.hide_replies)
|
||||
else
|
||||
if (comment.replyCount == 1)
|
||||
getString(R.string.view_reply)
|
||||
else
|
||||
getString(R.string.view_replies_count, comment.replyCount)
|
||||
}
|
||||
} else {
|
||||
viewBinding.commentTotalReplies.visibility = View.GONE
|
||||
viewBinding.commentRepliesDivider.visibility = View.GONE
|
||||
@@ -87,10 +96,15 @@ class CommentItem(val comment: Comment,
|
||||
if (repliesVisible) {
|
||||
repliesSection.clear()
|
||||
removeSubCommentIds()
|
||||
viewBinding.commentTotalReplies.text = "View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
|
||||
viewBinding.commentTotalReplies.context.run {
|
||||
viewBinding.commentTotalReplies.text = if (comment.replyCount == 1)
|
||||
getString(R.string.view_reply)
|
||||
else
|
||||
getString(R.string.view_replies_count, comment.replyCount)
|
||||
}
|
||||
repliesVisible = false
|
||||
} else {
|
||||
viewBinding.commentTotalReplies.text = "Hide Replies"
|
||||
viewBinding.commentTotalReplies.setText(R.string.hide_replies)
|
||||
repliesSection.clear()
|
||||
commentsFragment.viewReplyCallback(this)
|
||||
repliesVisible = true
|
||||
@@ -99,16 +113,20 @@ class CommentItem(val comment: Comment,
|
||||
|
||||
viewBinding.commentUserName.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
commentsFragment.activity,
|
||||
Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
.putExtra("userId", comment.userId.toInt())
|
||||
.putExtra("userLVL","[${levelColor.second}]"), null
|
||||
.putExtra("userLVL", "[${levelColor.second}]"),
|
||||
null
|
||||
)
|
||||
}
|
||||
viewBinding.commentUserAvatar.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
commentsFragment.activity,
|
||||
Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
.putExtra("userId", comment.userId.toInt())
|
||||
.putExtra("userLVL","[${levelColor.second}]"), null
|
||||
.putExtra("userLVL", "[${levelColor.second}]"),
|
||||
null
|
||||
)
|
||||
}
|
||||
viewBinding.commentText.setOnLongClickListener {
|
||||
@@ -127,39 +145,73 @@ class CommentItem(val comment: Comment,
|
||||
}
|
||||
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
|
||||
viewBinding.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
|
||||
viewBinding.commentDelete.setOnClickListener {
|
||||
dialogBuilder("Delete Comment", "Are you sure you want to delete this comment?") {
|
||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
scope.launch {
|
||||
val success = CommentsAPI.deleteComment(comment.commentId)
|
||||
if (success) {
|
||||
snackString("Comment Deleted")
|
||||
parentSection.remove(this@CommentItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewBinding.commentBanUser.setOnClickListener {
|
||||
dialogBuilder("Ban User", "Are you sure you want to ban this user?") {
|
||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
scope.launch {
|
||||
val success = CommentsAPI.banUser(comment.userId)
|
||||
if (success) {
|
||||
snackString("User Banned")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewBinding.commentReport.setOnClickListener {
|
||||
dialogBuilder("Report Comment", "Only report comments that violate the rules. Are you sure you want to report this comment?") {
|
||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
scope.launch {
|
||||
val success = CommentsAPI.reportComment(comment.commentId, comment.username, commentsFragment.mediaName, comment.userId)
|
||||
if (success) {
|
||||
snackString("Comment Reported")
|
||||
viewBinding.commentInfo.setOnClickListener {
|
||||
val popup = PopupMenu(commentsFragment.requireContext(), viewBinding.commentInfo)
|
||||
popup.menuInflater.inflate(R.menu.profile_details_menu, popup.menu)
|
||||
popup.menu.findItem(R.id.commentDelete)?.isVisible =
|
||||
isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod
|
||||
popup.menu.findItem(R.id.commentBanUser)?.isVisible =
|
||||
(CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment
|
||||
popup.menu.findItem(R.id.commentReport)?.isVisible = !isUserComment
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.commentReport -> {
|
||||
dialogBuilder(
|
||||
getAppString(R.string.report_comment),
|
||||
getAppString(R.string.report_comment_confirm)
|
||||
) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.reportComment(
|
||||
comment.commentId,
|
||||
comment.username,
|
||||
commentsFragment.mediaName,
|
||||
comment.userId
|
||||
)
|
||||
if (success) {
|
||||
snackString(R.string.comment_reported)
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.commentDelete -> {
|
||||
dialogBuilder(
|
||||
getAppString(R.string.delete_comment),
|
||||
getAppString(R.string.delete_comment_confirm)
|
||||
) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.deleteComment(comment.commentId)
|
||||
if (success) {
|
||||
snackString(R.string.comment_deleted)
|
||||
parentSection.remove(this@CommentItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.commentBanUser -> {
|
||||
dialogBuilder(
|
||||
getAppString(R.string.ban_user),
|
||||
getAppString(R.string.ban_user_confirm)
|
||||
) {
|
||||
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
|
||||
val success = CommentsAPI.banUser(comment.userId)
|
||||
if (success) {
|
||||
snackString(R.string.user_banned)
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
//fill the icon if the user has liked the comment
|
||||
setVoteButtons(viewBinding)
|
||||
@@ -195,7 +247,6 @@ class CommentItem(val comment: Comment,
|
||||
comment.upvotes -= 1
|
||||
}
|
||||
comment.downvotes += if (voteType == -1) 1 else -1
|
||||
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
@@ -210,7 +261,8 @@ class CommentItem(val comment: Comment,
|
||||
}
|
||||
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
|
||||
viewBinding.commentUserName.text = comment.username
|
||||
viewBinding.commentUserLevel.text = "[${levelColor.second}]"
|
||||
val userColor = "[${levelColor.second}]"
|
||||
viewBinding.commentUserLevel.text = userColor
|
||||
viewBinding.commentUserLevel.setTextColor(levelColor.first)
|
||||
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
|
||||
}
|
||||
@@ -228,12 +280,16 @@ class CommentItem(val comment: Comment,
|
||||
}
|
||||
|
||||
fun replying(isReplying: Boolean) {
|
||||
binding.commentReply.text = if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
|
||||
binding.commentReply.text =
|
||||
if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
|
||||
this.isReplying = isReplying
|
||||
}
|
||||
|
||||
fun editing(isEditing: Boolean) {
|
||||
binding.commentEdit.text = if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(R.string.edit)
|
||||
binding.commentEdit.text =
|
||||
if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(
|
||||
R.string.edit
|
||||
)
|
||||
this.isEditing = isEditing
|
||||
}
|
||||
|
||||
@@ -241,8 +297,9 @@ class CommentItem(val comment: Comment,
|
||||
subCommentIds.add(id)
|
||||
}
|
||||
|
||||
private fun removeSubCommentIds(){
|
||||
private fun removeSubCommentIds() {
|
||||
subCommentIds.forEach { id ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
|
||||
val commentToRemove = parentComments.find { it.comment.commentId == id }
|
||||
commentToRemove?.let {
|
||||
@@ -260,11 +317,13 @@ class CommentItem(val comment: Comment,
|
||||
viewBinding.commentUpVote.alpha = 1f
|
||||
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||
}
|
||||
|
||||
-1 -> {
|
||||
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
||||
viewBinding.commentDownVote.alpha = 1f
|
||||
}
|
||||
|
||||
else -> {
|
||||
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||
@@ -272,9 +331,10 @@ class CommentItem(val comment: Comment,
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun formatTimestamp(timestamp: String): String {
|
||||
return try {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val parsedDate = dateFormat.parse(timestamp)
|
||||
val currentDate = Date()
|
||||
@@ -297,8 +357,9 @@ class CommentItem(val comment: Comment,
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun timestampToMillis(timestamp: String): Long {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val parsedDate = dateFormat.parse(timestamp)
|
||||
return parsedDate?.time ?: 0
|
||||
@@ -307,7 +368,8 @@ class CommentItem(val comment: Comment,
|
||||
|
||||
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair<Int, Int> {
|
||||
val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt()
|
||||
val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
|
||||
val colorString =
|
||||
if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
|
||||
var color = Color.parseColor(colorString)
|
||||
val ratio = getContrastRatio(color, backgroundColor)
|
||||
if (ratio < 4.5) {
|
||||
@@ -325,16 +387,17 @@ class CommentItem(val comment: Comment,
|
||||
* @param callback the callback to call when the user clicks yes
|
||||
*/
|
||||
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
|
||||
val alertDialog = android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton("Yes") { dialog, _ ->
|
||||
callback()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
val alertDialog =
|
||||
android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton("Yes") { dialog, _ ->
|
||||
callback()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = alertDialog.show()
|
||||
dialog?.window?.setDimAmount(0.8f)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user