mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-18 00:23:55 +00:00
Compare commits
162 Commits
custom-dow
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fdd8d5d6e | ||
|
|
fda68a7ca2 | ||
|
|
40c2989b34 | ||
|
|
636a56fb7f | ||
|
|
abcf9fcbef | ||
|
|
b187cf06be | ||
|
|
14f29d09df | ||
|
|
390c709f5d | ||
|
|
7aa0951cdf | ||
|
|
aae80f6493 | ||
|
|
f86086cc7a | ||
|
|
425ca158a3 | ||
|
|
7bdc7c1719 | ||
|
|
126fe75c46 | ||
|
|
dc19694d68 | ||
|
|
95c0b574b0 | ||
|
|
a1b9f90500 | ||
|
|
d5be21882e | ||
|
|
e9551be62d | ||
|
|
3a88656e21 | ||
|
|
97ff591b62 | ||
|
|
a3e1cc45b3 | ||
|
|
deda67a070 | ||
|
|
e32bfa0cfa | ||
|
|
f03af6856a | ||
|
|
8a0224e6b0 | ||
|
|
71870ff235 | ||
|
|
31c509f88c | ||
|
|
4362dd94c1 | ||
|
|
9b132e9cb6 | ||
|
|
1431027cf3 | ||
|
|
2d2f058d27 | ||
|
|
85835b5c2e | ||
|
|
08c3806d0d | ||
|
|
74fcd581b0 | ||
|
|
0ea1ec1c35 | ||
|
|
42330032c8 | ||
|
|
fdb7f45a3d | ||
|
|
390ce9a022 | ||
|
|
95a9d289c9 | ||
|
|
69fb86d015 | ||
|
|
4fc96b77e3 | ||
|
|
040b0845de | ||
|
|
eca38070cb | ||
|
|
e9a60eafb6 | ||
|
|
0967721897 | ||
|
|
ed0e06d1af | ||
|
|
a7589a0296 | ||
|
|
e30124d342 | ||
|
|
140737bb40 | ||
|
|
8b582a9d32 | ||
|
|
0a0da65f7c | ||
|
|
ea48809d07 | ||
|
|
a573fbdc89 | ||
|
|
c947dbdb70 | ||
|
|
90b9b7bef3 | ||
|
|
133354a22d | ||
|
|
73ef5f4bbc | ||
|
|
140dd2e0c3 | ||
|
|
90e611dc4f | ||
|
|
7ecdbfd42f | ||
|
|
f4c95b6cc0 | ||
|
|
da456d3067 | ||
|
|
856deb7755 | ||
|
|
55ad8dccad | ||
|
|
e81773f2b5 | ||
|
|
c5a03c4455 | ||
|
|
d2127f92a1 | ||
|
|
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 | ||
|
|
012b1cd79d | ||
|
|
ff3131d988 |
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
|
||||
@@ -21,14 +21,7 @@ android {
|
||||
versionName "3.0.0"
|
||||
versionCode 300000000
|
||||
signingConfig signingConfigs.debug
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
flavorDimensions += "store"
|
||||
@@ -58,7 +51,7 @@ android {
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".beta"
|
||||
versionNameSuffix "-beta02"
|
||||
versionNameSuffix "-beta04"
|
||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
|
||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
|
||||
debuggable false
|
||||
@@ -88,14 +81,14 @@ 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'
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:33.0.0')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.0.0'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.0.0'
|
||||
// Core
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.browser:browser:1.8.0'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.7.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
@@ -106,7 +99,7 @@ dependencies {
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||
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 'androidx.webkit:webkit:1.11.0'
|
||||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
|
||||
// Glide
|
||||
@@ -118,7 +111,7 @@ dependencies {
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.3.0'
|
||||
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"
|
||||
@@ -130,7 +123,7 @@ dependencies {
|
||||
implementation "androidx.mediarouter:mediarouter:1.7.0"
|
||||
|
||||
// UI
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'com.google.android.material:material:1.12.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'
|
||||
@@ -138,17 +131,17 @@ dependencies {
|
||||
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
|
||||
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
|
||||
ext.groupie_version = '2.10.1'
|
||||
@@ -158,9 +151,6 @@ dependencies {
|
||||
// String Matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
|
||||
implementation group: 'com.arthenica', name: 'ffmpeg-kit-full-gpl', version: '6.0-2.LTS'
|
||||
//implementation 'com.github.yausername.youtubedl-android:library:0.15.0'
|
||||
|
||||
// Aniyomi
|
||||
implementation 'io.reactivex:rxjava:1.3.8'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
|
||||
@@ -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" />
|
||||
@@ -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" />
|
||||
@@ -79,7 +92,7 @@
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.statistics.ProfileStatsWidget"
|
||||
android:exported="false">
|
||||
@@ -123,6 +136,36 @@
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAboutActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".home.status.StatusActivity"
|
||||
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"
|
||||
@@ -131,7 +174,7 @@
|
||||
android:name=".widgets.statistics.ProfileStatsConfigure"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -174,6 +217,9 @@
|
||||
android:label="@string/manga"
|
||||
android:launchMode="singleTask" />
|
||||
<activity android:name=".media.GenreActivity" />
|
||||
<activity
|
||||
android:name=".media.MediaListViewActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".media.MediaDetailsActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
@@ -181,6 +227,11 @@
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity android:name=".media.CharacterDetailsActivity" />
|
||||
<activity android:name=".home.NoInternet" />
|
||||
<activity android:name=".others.CrashActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:process=":error_process"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".media.anime.ExoplayerView"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
@@ -331,7 +382,7 @@
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
android:theme="@style/Theme.AppCompat" />
|
||||
|
||||
<receiver
|
||||
android:name=".notifications.AlarmPermissionStateReceiver"
|
||||
@@ -394,6 +445,11 @@
|
||||
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".addons.torrent.TorrentServerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
|
||||
@@ -4,8 +4,11 @@ import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
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
|
||||
@@ -26,7 +29,9 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
@@ -34,6 +39,7 @@ import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@@ -41,6 +47,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)
|
||||
@@ -52,6 +61,7 @@ class App : MultiDexApplication() {
|
||||
|
||||
val mFTActivityLifecycleCallbacks = FTActivityLifecycleCallbacks()
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@@ -84,7 +94,7 @@ class App : MultiDexApplication() {
|
||||
|
||||
Logger.init(this)
|
||||
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
||||
Logger.log("App: Logging started")
|
||||
Logger.log(Log.WARN, "App: Logging started")
|
||||
|
||||
initializeNetwork()
|
||||
|
||||
@@ -93,37 +103,40 @@ class App : MultiDexApplication() {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
}
|
||||
|
||||
animeExtensionManager = Injekt.get()
|
||||
mangaExtensionManager = Injekt.get()
|
||||
novelExtensionManager = Injekt.get()
|
||||
|
||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
animeExtensionManager = Injekt.get()
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||
mangaScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
mangaExtensionManager = Injekt.get()
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val novelScope = CoroutineScope(Dispatchers.Default)
|
||||
novelScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
novelExtensionManager = Injekt.get()
|
||||
novelExtensionManager.findAvailableExtensions()
|
||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val commentsScope = CoroutineScope(Dispatchers.Default)
|
||||
commentsScope.launch {
|
||||
CommentsAPI.fetchAuthToken()
|
||||
}
|
||||
GlobalScope.launch {
|
||||
torrentAddonManager = Injekt.get()
|
||||
downloadAddonManager = Injekt.get()
|
||||
torrentAddonManager.init()
|
||||
downloadAddonManager.init()
|
||||
CommentsAPI.fetchAuthToken(this@App)
|
||||
|
||||
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
|
||||
val scheduler = TaskScheduler.create(this, useAlarmManager)
|
||||
scheduler.scheduleAllTasks(this)
|
||||
scheduler.scheduleSingleWork(this)
|
||||
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
|
||||
val scheduler = TaskScheduler.create(this@App, useAlarmManager)
|
||||
try {
|
||||
scheduler.scheduleAllTasks(this@App)
|
||||
} catch (e: IllegalStateException) {
|
||||
Logger.log("Failed to schedule tasks")
|
||||
Logger.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNotificationChannels() {
|
||||
@@ -153,7 +166,7 @@ class App : MultiDexApplication() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var instance: App? = null
|
||||
var instance: App? = null
|
||||
|
||||
/** Reference to the application context.
|
||||
*
|
||||
|
||||
@@ -16,7 +16,6 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.content.res.Resources.getSystem
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
@@ -68,9 +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.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
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
|
||||
@@ -92,6 +91,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
|
||||
@@ -102,6 +102,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
|
||||
@@ -134,9 +135,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
|
||||
@@ -184,9 +188,10 @@ fun currActivity(): Activity? {
|
||||
var loadMedia: Int? = null
|
||||
var loadIsMAL = false
|
||||
|
||||
val Int.toPx get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
|
||||
).toInt()
|
||||
val Int.toPx
|
||||
get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
|
||||
).toInt()
|
||||
|
||||
fun initActivity(a: Activity) {
|
||||
val window = a.window
|
||||
@@ -215,7 +220,8 @@ fun initActivity(a: Activity) {
|
||||
window.decorView
|
||||
).hide(WindowInsetsCompat.Type.statusBars())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
|
||||
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
) {
|
||||
window.decorView.rootWindowInsets?.displayCutout?.apply {
|
||||
if (boundingRects.size > 0) {
|
||||
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
|
||||
@@ -291,7 +297,12 @@ 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)
|
||||
setPadding(
|
||||
paddingLeft,
|
||||
paddingTop,
|
||||
paddingRight,
|
||||
navBarHeight + navBar.measuredHeight + overlayView.measuredHeight
|
||||
)
|
||||
}
|
||||
|
||||
fun Activity.reloadActivity() {
|
||||
@@ -301,23 +312,19 @@ fun Activity.reloadActivity() {
|
||||
initActivity(this)
|
||||
}
|
||||
|
||||
fun Context.restartApp(view: View) {
|
||||
fun Activity.restartApp() {
|
||||
val mainIntent = Intent.makeRestartActivityTask(
|
||||
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
|
||||
)
|
||||
val component = ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
|
||||
Snackbar.make(view, R.string.restart_app, Snackbar.LENGTH_INDEFINITE).apply {
|
||||
setAction(R.string.do_it) {
|
||||
this.dismiss()
|
||||
try {
|
||||
startActivity(Intent().setComponent(component))
|
||||
} catch (anything: Exception) {
|
||||
startActivity(mainIntent)
|
||||
}
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
show()
|
||||
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() {
|
||||
@@ -335,14 +342,7 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
val typedValue = TypedValue()
|
||||
val theme = requireContext().theme
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSurface,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
window.navigationBarColor = typedValue.data
|
||||
window.navigationBarColor = requireContext().getThemeColor(com.google.android.material.R.attr.colorSurface)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +464,7 @@ class InputFilterMinMax(
|
||||
}
|
||||
|
||||
|
||||
class ZoomOutPageTransformer() :
|
||||
class ZoomOutPageTransformer :
|
||||
ViewPager2.PageTransformer {
|
||||
override fun transformPage(view: View, position: Float) {
|
||||
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
|
||||
@@ -632,6 +632,23 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -953,7 +970,8 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
||||
|
||||
fun countDown(media: Media, view: ViewGroup) {
|
||||
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
|
||||
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
|
||||
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()
|
||||
) {
|
||||
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
@@ -985,6 +1003,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) {
|
||||
@@ -1296,24 +1358,40 @@ fun blurImage(imageView: ImageView, banner: String?) {
|
||||
if (banner != null) {
|
||||
val radius = PrefManager.getVal<Float>(PrefName.BlurRadius).toInt()
|
||||
val sampling = PrefManager.getVal<Float>(PrefName.BlurSampling).toInt()
|
||||
if (PrefManager.getVal(PrefName.BlurBanners)) {
|
||||
val context = imageView.context
|
||||
if (!(context as Activity).isDestroyed) {
|
||||
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
|
||||
val context = imageView.context
|
||||
if (!(context as Activity).isDestroyed) {
|
||||
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
|
||||
if (PrefManager.getVal(PrefName.BlurBanners)) {
|
||||
Glide.with(context as Context)
|
||||
.load(GlideUrl(url))
|
||||
.load(
|
||||
if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
|
||||
url
|
||||
) else File(url)
|
||||
)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
|
||||
.into(imageView)
|
||||
|
||||
} else {
|
||||
Glide.with(context as Context)
|
||||
.load(
|
||||
if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
|
||||
url
|
||||
) else File(url)
|
||||
)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
|
||||
.into(imageView)
|
||||
}
|
||||
} else {
|
||||
imageView.loadImage(banner)
|
||||
}
|
||||
} else {
|
||||
imageView.setImageResource(R.drawable.linear_gradient_bg)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getThemeColor(@AttrRes attribute: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(attribute, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
||||
/**
|
||||
* Builds the markwon instance with all the plugins
|
||||
* @return the markwon instance
|
||||
@@ -1387,4 +1465,4 @@ fun buildMarkwon(
|
||||
}))
|
||||
.build()
|
||||
return markwon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package ani.dantotsu
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Animatable
|
||||
@@ -14,13 +13,11 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.TextView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -35,22 +32,21 @@ 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.TorrentAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentServerService
|
||||
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
|
||||
import ani.dantotsu.home.MangaFragment
|
||||
import ani.dantotsu.home.NoInternet
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
||||
import ani.dantotsu.notifications.comment.CommentNotificationWorker
|
||||
import ani.dantotsu.notifications.TaskScheduler
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.FeedActivity
|
||||
@@ -70,11 +66,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 +85,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private var load = false
|
||||
|
||||
|
||||
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -100,12 +99,7 @@ class MainActivity : AppCompatActivity() {
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
androidx.work.WorkManager.getInstance(this)
|
||||
.enqueue(OneTimeWorkRequest.Companion.from(CommentNotificationWorker::class.java))
|
||||
|
||||
androidx.work.WorkManager.getInstance(this)
|
||||
.enqueue(OneTimeWorkRequest.Companion.from(AnilistNotificationWorker::class.java))
|
||||
TaskScheduler.scheduleSingleWork(this)
|
||||
|
||||
val action = intent.action
|
||||
val type = intent.type
|
||||
@@ -299,6 +293,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
var launched = false
|
||||
intent.extras?.let { extras ->
|
||||
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
|
||||
val mediaId = extras.getInt("mediaId", -1)
|
||||
@@ -311,6 +306,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 {
|
||||
@@ -318,6 +314,7 @@ class MainActivity : AppCompatActivity() {
|
||||
putExtra("activityId", activityId)
|
||||
|
||||
}
|
||||
launched = true
|
||||
startActivity(feedIntent)
|
||||
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
||||
Logger.log("MainActivity, onCreate: $activityId")
|
||||
@@ -325,6 +322,7 @@ class MainActivity : AppCompatActivity() {
|
||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
||||
putExtra("activityId", activityId)
|
||||
}
|
||||
launched = true
|
||||
startActivity(notificationIntent)
|
||||
}
|
||||
}
|
||||
@@ -338,47 +336,39 @@ class MainActivity : AppCompatActivity() {
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
if (mainViewPager.currentItem != selectedOption) {
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
selectedOption,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
}
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
if (mainViewPager.currentItem != selectedOption) {
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
selectedOption,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load) {
|
||||
if (!load && !launched) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMain(this@MainActivity)
|
||||
val id = intent.extras?.getInt("mediaId", 0)
|
||||
@@ -449,16 +439,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 (!TorrentServerService.isRunning()) {
|
||||
TorrentServerService.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/ //TODO: remove this
|
||||
}
|
||||
if (torrentManager.isInitialized.value == false) {
|
||||
torrentManager.isInitialized.observe(this) {
|
||||
if (it) {
|
||||
startTorrent()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startTorrent()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
@@ -469,7 +469,7 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
|
||||
val params : ViewGroup.MarginLayoutParams =
|
||||
val params: ViewGroup.MarginLayoutParams =
|
||||
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.updateMargins(bottom = margin.toPx)
|
||||
}
|
||||
@@ -478,16 +478,14 @@ class MainActivity : AppCompatActivity() {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView =
|
||||
LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null)
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
val 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')
|
||||
|
||||
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 for $repo: $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(andEnabled: Boolean = true): 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 { postValue(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(andEnabled: Boolean): 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,141 @@
|
||||
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.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
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 { postValue(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(andEnabled: Boolean): Boolean {
|
||||
return extension?.extension != null && if (andEnabled) {
|
||||
PrefManager.getVal(PrefName.TorrentEnabled)
|
||||
} else true
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
172
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
172
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
@@ -0,0 +1,172 @@
|
||||
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 TorrentServerService : Service() {
|
||||
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
|
||||
private val applicationContext = Injekt.get<Application>()
|
||||
private lateinit var extension: TorrentAddonApi
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
extension = Injekt.get<TorrentAddonManager>().extension?.extension ?: return START_NOT_STICKY
|
||||
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, TorrentServerService::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 (TorrentServerService::class.java.name.equals(it.service.className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (Injekt.get<TorrentAddonManager>().extension?.extension == null) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), TorrentServerService::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>(), TorrentServerService::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
|
||||
@@ -38,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,7 +3,6 @@ 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
|
||||
@@ -14,7 +13,6 @@ 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
|
||||
|
||||
@@ -37,6 +35,8 @@ object Anilist {
|
||||
|
||||
var rateLimitReset: Long = 0
|
||||
|
||||
var initialized = false
|
||||
|
||||
val sortBy = listOf(
|
||||
"SCORE_DESC",
|
||||
"POPULARITY_DESC",
|
||||
@@ -201,8 +201,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 (show) Logger.log("Anilist Response: ${json.text}")
|
||||
if (!json.text.startsWith("{")) {
|
||||
throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
}
|
||||
json.parsed()
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -20,10 +20,10 @@ 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
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -32,6 +32,7 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
import java.util.Calendar
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class AnilistQueries {
|
||||
@@ -56,6 +57,7 @@ class AnilistQueries {
|
||||
Anilist.unreadNotificationCount = user.unreadNotificationCount ?: 0
|
||||
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
|
||||
Anilist.unreadNotificationCount += unread
|
||||
Anilist.initialized = true
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -72,14 +74,14 @@ class AnilistQueries {
|
||||
media.cameFromContinue = false
|
||||
|
||||
val query =
|
||||
"""{Media(id:${media.id}){id favourites popularity 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)
|
||||
var response = executeQuery<Query.Media>(query, force = 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()
|
||||
@@ -139,7 +141,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>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -153,7 +163,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"
|
||||
@@ -200,7 +210,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
|
||||
@@ -345,70 +372,11 @@ class AnilistQueries {
|
||||
}
|
||||
return media
|
||||
}
|
||||
|
||||
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
val query = if (planned) {
|
||||
"""{ planned: ${continueMediaQuery(type, "PLANNING")} }"""
|
||||
} else {
|
||||
"""{
|
||||
current: ${continueMediaQuery(type, "CURRENT")},
|
||||
repeating: ${continueMediaQuery(type, "REPEATING")}
|
||||
}"""
|
||||
}
|
||||
|
||||
val response = executeQuery<Query.CombinedMediaListResponse>(query)
|
||||
if (planned) {
|
||||
response?.data?.planned?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response?.data?.current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
response?.data?.repeating?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type != "ANIME") {
|
||||
returnArray.addAll(map.values)
|
||||
return returnArray
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
List::class.java
|
||||
) as List<Int>
|
||||
if (list.isNotEmpty()) {
|
||||
list.reversed().forEach {
|
||||
if (map.containsKey(it)) returnArray.add(map[it]!!)
|
||||
}
|
||||
for (i in map) {
|
||||
if (i.value !in returnArray) returnArray.add(i.value)
|
||||
}
|
||||
} else returnArray.addAll(map.values)
|
||||
return returnArray
|
||||
}
|
||||
|
||||
private fun continueMediaQuery(type: String, status: String): String {
|
||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
|
||||
}
|
||||
|
||||
suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
|
||||
private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
|
||||
var hasNextPage = true
|
||||
var page = 0
|
||||
|
||||
@@ -436,41 +404,6 @@ class AnilistQueries {
|
||||
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
||||
}
|
||||
|
||||
suspend fun recommendations(): ArrayList<Media> {
|
||||
val response = executeQuery<Query.Page>("""{${recommendationQuery()}}""")
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
response?.data?.page?.apply {
|
||||
recommendations?.onEach {
|
||||
val json = it.mediaRecommendation
|
||||
if (json != null) {
|
||||
val m = Media(json)
|
||||
m.relation = json.type?.toString()
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val types = arrayOf("ANIME", "MANGA")
|
||||
suspend fun repeat(type: String) {
|
||||
val res =
|
||||
executeQuery<Query.MediaListCollection>("""{${recommendationPlannedQuery(type)}}""")
|
||||
res?.data?.mediaListCollection?.lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.status == "RELEASING" || m.status == "FINISHED") {
|
||||
m.relation = it.media?.type?.toString()
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
types.forEach { repeat(it) }
|
||||
|
||||
val list = ArrayList(map.values.toList())
|
||||
list.sortByDescending { it.meanScore }
|
||||
return list
|
||||
}
|
||||
|
||||
private fun recommendationQuery(): String {
|
||||
return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """
|
||||
}
|
||||
@@ -479,9 +412,11 @@ class AnilistQueries {
|
||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
|
||||
}
|
||||
|
||||
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
|
||||
suspend fun initHomePage(): Map<String, ArrayList<*>> {
|
||||
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
|
||||
val removedMedia = ArrayList<Media>()
|
||||
val toShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayoutShow) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations
|
||||
PrefManager.getVal(PrefName.HomeLayout) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations
|
||||
var query = """{"""
|
||||
if (toShow.getOrNull(0) == true) query += """currentAnime: ${
|
||||
continueMediaQuery(
|
||||
@@ -514,11 +449,11 @@ class AnilistQueries {
|
||||
"ANIME"
|
||||
)
|
||||
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
|
||||
if (toShow.getOrNull(7) == true) query += "Page1:${status(1)}Page2:${status(2)}"
|
||||
query += """}""".trimEnd(',')
|
||||
|
||||
val response = executeQuery<Query.HomePageMedia>(query, show = true)
|
||||
Logger.log(response.toString())
|
||||
val returnMap = mutableMapOf<String, ArrayList<Media>>()
|
||||
val returnMap = mutableMapOf<String, ArrayList<*>>()
|
||||
fun current(type: String) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
val returnArray = arrayListOf<Media>()
|
||||
@@ -529,15 +464,24 @@ class AnilistQueries {
|
||||
current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
if (m.id !in removeList) {
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
} else {
|
||||
removedMedia.add(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeating?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
if (m.id !in removeList) {
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
} else {
|
||||
removedMedia.add(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type != "Anime") {
|
||||
@@ -571,8 +515,12 @@ class AnilistQueries {
|
||||
current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
if (m.id !in removeList) {
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
} else {
|
||||
removedMedia.add(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -599,7 +547,12 @@ class AnilistQueries {
|
||||
val returnArray = arrayListOf<Media>()
|
||||
apiMediaList?.edges?.forEach {
|
||||
it.node?.let { i ->
|
||||
returnArray.add(Media(i).apply { isFav = true })
|
||||
val m = Media(i).apply { isFav = true }
|
||||
if (m.id !in removeList) {
|
||||
returnArray.add(m)
|
||||
} else {
|
||||
removedMedia.add(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
returnMap["favorite$type"] = returnArray
|
||||
@@ -661,6 +614,47 @@ class AnilistQueries {
|
||||
list.sortByDescending { it.meanScore }
|
||||
returnMap["recommendations"] = list
|
||||
}
|
||||
if (toShow.getOrNull(7) == true) {
|
||||
val list = mutableListOf<User>()
|
||||
val threeDaysAgo = Calendar.getInstance().apply {
|
||||
add(Calendar.DAY_OF_MONTH, -3)
|
||||
}.timeInMillis
|
||||
if (response?.data?.page1 != null && response.data.page2 != null) {
|
||||
val activities = listOf(
|
||||
response.data.page1.activities,
|
||||
response.data.page2.activities
|
||||
).asSequence().flatten()
|
||||
.filter { it.typename != "MessageActivity" }
|
||||
.filter { if (Anilist.adult) true else it.media?.isAdult == false }
|
||||
.filter { it.createdAt * 1000L > threeDaysAgo }.toList()
|
||||
.sortedByDescending { it.createdAt }
|
||||
val anilistActivities = mutableListOf<User>()
|
||||
val groupedActivities = activities.groupBy { it.userId }
|
||||
|
||||
groupedActivities.forEach { (_, userActivities) ->
|
||||
val user = userActivities.firstOrNull()?.user
|
||||
if (user != null) {
|
||||
val userToAdd = User(
|
||||
user.id,
|
||||
user.name ?: "",
|
||||
user.avatar?.medium,
|
||||
user.bannerImage,
|
||||
activity = userActivities.sortedBy { it.createdAt }.toList()
|
||||
)
|
||||
if (user.id == Anilist.userid) {
|
||||
anilistActivities.add(0, userToAdd)
|
||||
} else {
|
||||
list.add(userToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
list.addAll(0, anilistActivities)
|
||||
returnMap["status"] = ArrayList(list)
|
||||
}
|
||||
returnMap["hidden"] = removedMedia.distinctBy { it.id } as ArrayList<Media>
|
||||
}
|
||||
return returnMap
|
||||
}
|
||||
|
||||
@@ -1035,63 +1029,157 @@ 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(): String{
|
||||
return """Page(page:1,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 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(): String{
|
||||
return """Page(page:1,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 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(): String{
|
||||
return """Page(page:1,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 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(): String{
|
||||
return """Page(page:1,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}}}"""
|
||||
|
||||
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(): Query.AnimeList?{
|
||||
return executeQuery<Query.AnimeList>(
|
||||
"""{
|
||||
recentUpdates:${recentAnimeUpdates()}
|
||||
trendingMovies:${trendingMovies()}
|
||||
topRated:${topRatedAnime()}
|
||||
mostFav:${mostFavAnime()}
|
||||
}""".trimIndent(), force = true
|
||||
|
||||
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 val onListManga = (if(PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace("\"", "")
|
||||
private fun trendingManga(): String{
|
||||
return """Page(page:1,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(): String{
|
||||
return """Page(page:1,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(): String{
|
||||
return """Page(page:1,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(): String{
|
||||
return """Page(page:1,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(): String{
|
||||
return """Page(page:1,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(): Query.MangaList?{
|
||||
return executeQuery<Query.MangaList>(
|
||||
"""{
|
||||
trendingManga:${trendingManga()}
|
||||
trendingManhwa:${trendingManhwa()}
|
||||
trendingNovel:${trendingNovel()}
|
||||
topRated:${topRatedManga()}
|
||||
mostFav:${mostFavManga()}
|
||||
}""".trimIndent(), force = true
|
||||
)
|
||||
|
||||
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(
|
||||
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) {
|
||||
@@ -1138,25 +1226,26 @@ Page(page:$page,perPage:50) {
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
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()
|
||||
while (res?.pageInfo?.hasNextPage == true) {
|
||||
next()
|
||||
i++
|
||||
}
|
||||
return list.reversed().toMutableList()
|
||||
i++
|
||||
}
|
||||
return list.reversed().toMutableList()
|
||||
}
|
||||
|
||||
suspend fun getCharacterDetails(character: Character): Character {
|
||||
@@ -1342,19 +1431,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()
|
||||
@@ -1369,6 +1478,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")) {
|
||||
@@ -1376,6 +1499,7 @@ Page(page:$page,perPage:50) {
|
||||
yearMedia.remove("CANCELLED")
|
||||
yearMedia["CANCELLED"] = a
|
||||
}
|
||||
author.character = characters
|
||||
author.yearMedia = yearMedia
|
||||
return author
|
||||
}
|
||||
@@ -1404,7 +1528,7 @@ Page(page:$page,perPage:50) {
|
||||
return getUserProfile(id)
|
||||
}
|
||||
|
||||
suspend fun getUserId(username: String): Int? {
|
||||
private suspend fun getUserId(username: String): Int? {
|
||||
return executeQuery<Query.User>(
|
||||
"""{User(name:"$username"){id}}""",
|
||||
force = true
|
||||
@@ -1419,8 +1543,8 @@ Page(page:$page,perPage:50) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun userFavMediaQuery(anime: Boolean, page: Int, id: Int): String {
|
||||
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
||||
private fun userFavMediaQuery(anime: Boolean, id: Int): String {
|
||||
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:1){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
||||
}
|
||||
|
||||
suspend fun userFollowing(id: Int): Query.Following? {
|
||||
@@ -1440,14 +1564,18 @@ Page(page:$page,perPage:50) {
|
||||
suspend fun initProfilePage(id: Int): Query.ProfilePageMedia? {
|
||||
return executeQuery<Query.ProfilePageMedia>(
|
||||
"""{
|
||||
favoriteAnime:${userFavMediaQuery(true, 1, id)}
|
||||
favoriteManga:${userFavMediaQuery(false, 1, id)}
|
||||
favoriteAnime:${userFavMediaQuery(true, id)}
|
||||
favoriteManga:${userFavMediaQuery(false, id)}
|
||||
}""".trimIndent(), force = true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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,}}}}""",
|
||||
@@ -1462,16 +1590,24 @@ 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,"
|
||||
else "isFollowing:true,type_not:MESSAGE,"
|
||||
else "isFollowing:true,"
|
||||
return executeQuery<FeedResponse>(
|
||||
"""{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id title{english romaji native userPreferred}bannerImage coverImage{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id recipientId messengerId type replyCount likeCount message(asHtml:true)isLocked isSubscribed isLiked isPrivate siteUrl createdAt recipient{id name bannerImage avatar{medium large}}messenger{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}}}}""",
|
||||
force = true
|
||||
)
|
||||
}
|
||||
private fun status(page: Int = 1): String {
|
||||
return """Page(page:$page,perPage:50){activities(isFollowing: true,sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id isAdult title{english romaji native userPreferred}bannerImage coverImage{extraLarge medium large}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id type createdAt}}}"""
|
||||
}
|
||||
|
||||
suspend fun getUpcomingAnime(id: String): List<Media> {
|
||||
val res = executeQuery<Query.MediaListCollection>(
|
||||
@@ -1491,14 +1627,26 @@ Page(page:$page,perPage:50) {
|
||||
.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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,48 +5,34 @@ import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.webkit.internal.ApiFeature.P
|
||||
import androidx.webkit.internal.StartupApiFeature
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.profile.User
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||
val userid = PrefManager.getVal(PrefName.DiscordId, null as String?)
|
||||
if (userid == null && token != null) {
|
||||
/*if (!Discord.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_discord_user_data))*/
|
||||
//TODO: Discord.getUserData()
|
||||
}
|
||||
}
|
||||
|
||||
val anilist = if (Anilist.userid == null && Anilist.token != null) {
|
||||
if (!Anilist.initialized) {
|
||||
if (Anilist.query.getUserData()) {
|
||||
tryWithSuspend {
|
||||
if (MAL.token != null && !MAL.query.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_mal_user_data))
|
||||
}
|
||||
true
|
||||
} else {
|
||||
snackString(context.getString(R.string.error_loading_anilist_user_data))
|
||||
false
|
||||
}
|
||||
} else true
|
||||
|
||||
if (anilist) block.invoke()
|
||||
}
|
||||
block.invoke()
|
||||
}
|
||||
|
||||
class AnilistHomeViewModel : ViewModel() {
|
||||
@@ -91,16 +77,28 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
|
||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||
|
||||
private val userStatus: MutableLiveData<ArrayList<User>> =
|
||||
MutableLiveData<ArrayList<User>>(null)
|
||||
|
||||
fun getUserStatus(): LiveData<ArrayList<User>> = userStatus
|
||||
|
||||
private val hidden: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getHidden(): LiveData<ArrayList<Media>> = hidden
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun initHomePage() {
|
||||
val res = Anilist.query.initHomePage()
|
||||
Logger.log("AnilistHomeViewModel : res=$res")
|
||||
res["currentAnime"]?.let { animeContinue.postValue(it) }
|
||||
res["favoriteAnime"]?.let { animeFav.postValue(it) }
|
||||
res["plannedAnime"]?.let { animePlanned.postValue(it) }
|
||||
res["currentManga"]?.let { mangaContinue.postValue(it) }
|
||||
res["favoriteManga"]?.let { mangaFav.postValue(it) }
|
||||
res["plannedManga"]?.let { mangaPlanned.postValue(it) }
|
||||
res["recommendations"]?.let { recommendation.postValue(it) }
|
||||
res["currentAnime"]?.let { animeContinue.postValue(it as ArrayList<Media>?) }
|
||||
res["favoriteAnime"]?.let { animeFav.postValue(it as ArrayList<Media>?) }
|
||||
res["plannedAnime"]?.let { animePlanned.postValue(it as ArrayList<Media>?) }
|
||||
res["currentManga"]?.let { mangaContinue.postValue(it as ArrayList<Media>?) }
|
||||
res["favoriteManga"]?.let { mangaFav.postValue(it as ArrayList<Media>?) }
|
||||
res["plannedManga"]?.let { mangaPlanned.postValue(it as ArrayList<Media>?) }
|
||||
res["recommendations"]?.let { recommendation.postValue(it as ArrayList<Media>?) }
|
||||
res["hidden"]?.let { hidden.postValue(it as ArrayList<Media>?) }
|
||||
res["status"]?.let { userStatus.postValue(it as ArrayList<User>?) }
|
||||
}
|
||||
|
||||
suspend fun loadMain(context: FragmentActivity) {
|
||||
@@ -108,9 +106,15 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
MAL.getSavedToken()
|
||||
Discord.getSavedToken()
|
||||
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||
if (PrefManager.getVal(PrefName.CheckUpdate))
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
AppUpdater.check(context, false)
|
||||
}
|
||||
}
|
||||
val ret = Anilist.query.getGenresAndTags()
|
||||
withContext(Dispatchers.Main) {
|
||||
genres.value = ret
|
||||
}
|
||||
genres.postValue(Anilist.query.getGenresAndTags())
|
||||
}
|
||||
|
||||
val empty = MutableLiveData<Boolean>(null)
|
||||
@@ -189,46 +193,29 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
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 res = Anilist.query.loadAnimeList()?.data
|
||||
|
||||
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
|
||||
res?.apply{
|
||||
val idArr = mutableListOf<Int>()
|
||||
updated.postValue(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
|
||||
}
|
||||
}?.toMutableList() ?: arrayListOf())
|
||||
popularMovies.postValue(trendingMovies?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
topRatedAnime.postValue(topRated?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
mostFavAnime.postValue(mostFav?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
}
|
||||
val list = Anilist.query.loadAnimeList()
|
||||
updated.postValue(list["recentUpdates"])
|
||||
popularMovies.postValue(list["trendingMovies"])
|
||||
topRatedAnime.postValue(list["topRated"])
|
||||
mostFavAnime.postValue(list["mostFav"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,33 +290,35 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
|
||||
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 response = Anilist.query.loadMangaList()?.data
|
||||
|
||||
response?.apply {
|
||||
popularManga.postValue(trendingManga?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
popularManhwa.postValue(trendingManhwa?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
popularNovel.postValue(trendingNovel?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
topRatedManga.postValue(topRated?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
mostFavManga.postValue(mostFav?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
|
||||
}
|
||||
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"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +11,7 @@ data class SearchResults(
|
||||
var onList: Boolean? = null,
|
||||
var perPage: Int? = null,
|
||||
var search: String? = null,
|
||||
var countryOfOrigin :String? = null,
|
||||
var countryOfOrigin: String? = null,
|
||||
var sort: String? = null,
|
||||
var genres: MutableList<String>? = null,
|
||||
var excludedGenres: MutableList<String>? = null,
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -136,6 +138,8 @@ class Query {
|
||||
@SerialName("recommendationQuery") val recommendationQuery: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("recommendationPlannedQueryAnime") val recommendationPlannedQueryAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("recommendationPlannedQueryManga") val recommendationPlannedQueryManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("Page1") val page1: ActivityPage?,
|
||||
@SerialName("Page2") val page2: ActivityPage?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -147,8 +151,10 @@ 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("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AnimeList(
|
||||
@SerialName("data")
|
||||
@@ -157,11 +163,16 @@ class Query {
|
||||
@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")
|
||||
@@ -170,12 +181,18 @@ class Query {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ToggleFollow(
|
||||
@SerialName("data")
|
||||
@@ -306,13 +323,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(
|
||||
@@ -323,9 +340,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,
|
||||
@@ -337,13 +354,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(
|
||||
@@ -351,13 +368,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(
|
||||
@@ -369,19 +386,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(
|
||||
@@ -389,7 +406,7 @@ class Query {
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
//----------------------------------------
|
||||
// Statistics
|
||||
@@ -398,12 +415,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
|
||||
|
||||
@@ -14,7 +14,6 @@ data class FeedResponse(
|
||||
val page: ActivityPage
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ActivityPage(
|
||||
@SerialName("activities")
|
||||
@@ -36,7 +35,7 @@ data class Activity(
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("replyCount")
|
||||
val replyCount: Int,
|
||||
val replyCount: Int = 0,
|
||||
@SerialName("status")
|
||||
val status: String?,
|
||||
@SerialName("progress")
|
||||
@@ -48,9 +47,9 @@ data class Activity(
|
||||
@SerialName("siteUrl")
|
||||
val siteUrl: String?,
|
||||
@SerialName("isLocked")
|
||||
val isLocked: Boolean,
|
||||
val isLocked: Boolean?,
|
||||
@SerialName("isSubscribed")
|
||||
val isSubscribed: Boolean,
|
||||
val isSubscribed: Boolean?,
|
||||
@SerialName("likeCount")
|
||||
var likeCount: Int?,
|
||||
@SerialName("isLiked")
|
||||
@@ -84,9 +83,9 @@ data class ActivityReply(
|
||||
@SerialName("text")
|
||||
val text: String,
|
||||
@SerialName("likeCount")
|
||||
val likeCount: Int,
|
||||
var likeCount: Int,
|
||||
@SerialName("isLiked")
|
||||
val isLiked: Boolean,
|
||||
var isLiked: Boolean,
|
||||
@SerialName("createdAt")
|
||||
val createdAt: Int,
|
||||
@SerialName("user")
|
||||
|
||||
@@ -174,7 +174,7 @@ data class Media(
|
||||
|
||||
// Notes for site moderators
|
||||
@SerialName("modNotes") var modNotes: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MediaTitle(
|
||||
@@ -189,7 +189,7 @@ data class MediaTitle(
|
||||
|
||||
// The currently authenticated users preferred title language. Default romaji for non-authenticated
|
||||
@SerialName("userPreferred") var userPreferred: String,
|
||||
)
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class MediaType {
|
||||
@@ -205,15 +205,17 @@ enum class MediaStatus {
|
||||
FINISHED, RELEASING, NOT_YET_RELEASED, CANCELLED, HIATUS;
|
||||
|
||||
override fun toString(): String {
|
||||
return when (super.toString()) {
|
||||
"FINISHED" -> currContext()!!.getString(R.string.status_finished)
|
||||
"RELEASING" -> currContext()!!.getString(R.string.status_releasing)
|
||||
"NOT_YET_RELEASED" -> currContext()!!.getString(R.string.status_not_yet_released)
|
||||
"CANCELLED" -> currContext()!!.getString(R.string.status_cancelled)
|
||||
"HIATUS" -> currContext()!!.getString(R.string.status_hiatus)
|
||||
else -> ""
|
||||
currContext()?.let {
|
||||
return when (super.toString()) {
|
||||
"FINISHED" -> it.getString(R.string.status_finished)
|
||||
"RELEASING" -> it.getString(R.string.status_releasing)
|
||||
"NOT_YET_RELEASED" -> it.getString(R.string.status_not_yet_released)
|
||||
"CANCELLED" -> it.getString(R.string.status_cancelled)
|
||||
"HIATUS" -> it.getString(R.string.status_hiatus)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
return super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,17 +447,20 @@ enum class MediaRelation {
|
||||
ADAPTATION, PREQUEL, SEQUEL, PARENT, SIDE_STORY, CHARACTER, SUMMARY, ALTERNATIVE, SPIN_OFF, OTHER, SOURCE, COMPILATION, CONTAINS;
|
||||
|
||||
override fun toString(): String {
|
||||
return when (super.toString()) {
|
||||
"ADAPTATION" -> currContext()!!.getString(R.string.type_adaptation)
|
||||
"PARENT" -> currContext()!!.getString(R.string.type_parent)
|
||||
"CHARACTER" -> currContext()!!.getString(R.string.type_character)
|
||||
"SUMMARY" -> currContext()!!.getString(R.string.type_summary)
|
||||
"ALTERNATIVE" -> currContext()!!.getString(R.string.type_alternative)
|
||||
"OTHER" -> currContext()!!.getString(R.string.type_other)
|
||||
"SOURCE" -> currContext()!!.getString(R.string.type_source)
|
||||
"CONTAINS" -> currContext()!!.getString(R.string.type_contains)
|
||||
else -> super.toString().replace("_", " ")
|
||||
currContext()?.let {
|
||||
return when (super.toString()) {
|
||||
"ADAPTATION" -> it.getString(R.string.type_adaptation)
|
||||
"PARENT" -> it.getString(R.string.type_parent)
|
||||
"CHARACTER" -> it.getString(R.string.type_character)
|
||||
"SUMMARY" -> it.getString(R.string.type_summary)
|
||||
"ALTERNATIVE" -> it.getString(R.string.type_alternative)
|
||||
"OTHER" -> it.getString(R.string.type_other)
|
||||
"SOURCE" -> it.getString(R.string.type_source)
|
||||
"CONTAINS" -> it.getString(R.string.type_contains)
|
||||
else -> super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
return super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -69,7 +69,7 @@ data class User(
|
||||
// The user's previously used names.
|
||||
// @SerialName("previousNames") var previousNames: List<UserPreviousName>?,
|
||||
|
||||
)
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserOptions(
|
||||
@@ -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,133 @@
|
||||
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 = try {
|
||||
client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e.toString())
|
||||
return@tryWithSuspend null
|
||||
}
|
||||
coroutineScope {
|
||||
res.results?.map {
|
||||
async(Dispatchers.IO) {
|
||||
Logger.log(it.toString())
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
res.results?.first {
|
||||
it.metadata.series.lastUpdated?.timestamp != null
|
||||
&& (it.metadata.series.latestChapter != null
|
||||
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
|
||||
return results.metadata.series.latestChapter?.let {
|
||||
context.getString(R.string.chapter_number, it)
|
||||
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
|
||||
chapter.takeIf {
|
||||
it.toIntOrNull() == null
|
||||
} ?: context.getString(R.string.chapter_number, chapter.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MangaUpdatesResponse(
|
||||
@SerialName("total_hits")
|
||||
val totalHits: Int?,
|
||||
@SerialName("page")
|
||||
val page: Int?,
|
||||
@SerialName("per_page")
|
||||
val perPage: Int?,
|
||||
val results: List<Results>? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Results(
|
||||
val record: Record,
|
||||
val metadata: MetaData
|
||||
) {
|
||||
@Serializable
|
||||
data class Record(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("title")
|
||||
val title: String,
|
||||
@SerialName("volume")
|
||||
val volume: String?,
|
||||
@SerialName("chapter")
|
||||
val chapter: String?,
|
||||
@SerialName("release_date")
|
||||
val releaseDate: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MetaData(
|
||||
val series: Series
|
||||
) {
|
||||
@Serializable
|
||||
data class Series(
|
||||
@SerialName("series_id")
|
||||
val seriesId: Long?,
|
||||
@SerialName("title")
|
||||
val title: String?,
|
||||
@SerialName("latest_chapter")
|
||||
val latestChapter: Int?,
|
||||
@SerialName("last_updated")
|
||||
val lastUpdated: LastUpdated?
|
||||
) {
|
||||
@Serializable
|
||||
data class LastUpdated(
|
||||
@SerialName("timestamp")
|
||||
val timestamp: Long,
|
||||
@SerialName("as_rfc3339")
|
||||
val asRfc3339: String,
|
||||
@SerialName("as_string")
|
||||
val asString: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package ani.dantotsu.connections.comments
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
@@ -25,6 +28,7 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
object CommentsAPI {
|
||||
private const val ADDRESS: String = "https://1224665.xyz:443"
|
||||
private var isOnline: Boolean = true
|
||||
var authToken: String? = null
|
||||
var userId: String? = null
|
||||
var isBanned: Boolean = false
|
||||
@@ -32,7 +36,12 @@ object CommentsAPI {
|
||||
var isMod: Boolean = false
|
||||
var totalVotes: Int = 0
|
||||
|
||||
suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? {
|
||||
suspend fun getCommentsForId(
|
||||
id: Int,
|
||||
page: Int = 1,
|
||||
tag: Int?,
|
||||
sort: String?
|
||||
): CommentResponse? {
|
||||
var url = "$ADDRESS/comments/$id/$page"
|
||||
val request = requestBuilder()
|
||||
tag?.let {
|
||||
@@ -44,7 +53,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.get(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to fetch comments")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to fetch comments")
|
||||
return null
|
||||
}
|
||||
if (!json.text.startsWith("{")) return null
|
||||
@@ -66,7 +76,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.get(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to fetch comments")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to fetch comments")
|
||||
return null
|
||||
}
|
||||
if (!json.text.startsWith("{")) return null
|
||||
@@ -88,7 +99,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.get(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to fetch comment")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to fetch comment")
|
||||
return null
|
||||
}
|
||||
if (!json.text.startsWith("{")) return null
|
||||
@@ -110,7 +122,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.post(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to vote")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to vote")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
@@ -136,7 +149,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.post(url, requestBody = body.build())
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to comment")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to comment")
|
||||
return null
|
||||
}
|
||||
val res = json.code == 200
|
||||
@@ -147,7 +161,8 @@ object CommentsAPI {
|
||||
val parsed = try {
|
||||
Json.decodeFromString<ReturnedComment>(json.text)
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to parse comment")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to parse comment")
|
||||
return null
|
||||
}
|
||||
return Comment(
|
||||
@@ -174,7 +189,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.delete(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to delete comment")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to delete comment")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
@@ -193,7 +209,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.put(url, requestBody = body)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to edit comment")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to edit comment")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
@@ -209,7 +226,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.post(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to ban user")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to ban user")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
@@ -236,7 +254,8 @@ object CommentsAPI {
|
||||
val json = try {
|
||||
request.post(url, requestBody = body)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to report comment")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to report comment")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
@@ -291,7 +310,8 @@ object CommentsAPI {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun fetchAuthToken(client: OkHttpClient? = null) {
|
||||
suspend fun fetchAuthToken(context: Context, client: OkHttpClient? = null) {
|
||||
isOnline = isOnline(context)
|
||||
if (authToken != null) return
|
||||
val MAX_RETRIES = 5
|
||||
val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days
|
||||
@@ -320,7 +340,8 @@ object CommentsAPI {
|
||||
val parsed = try {
|
||||
Json.decodeFromString<AuthResponse>(json.text)
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to login to comments API: ${e.printStackTrace()}")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to login to comments API: ${e.printStackTrace()}")
|
||||
return
|
||||
}
|
||||
PrefManager.setVal(PrefName.CommentAuthResponse, parsed)
|
||||
@@ -340,12 +361,18 @@ object CommentsAPI {
|
||||
return
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to login to comments API")
|
||||
Logger.log(e)
|
||||
errorMessage("Failed to login to comments API")
|
||||
return
|
||||
}
|
||||
kotlinx.coroutines.delay(60000)
|
||||
}
|
||||
snackString("Failed to login after multiple attempts")
|
||||
errorMessage("Failed to login after multiple attempts")
|
||||
}
|
||||
|
||||
private fun errorMessage(reason: String) {
|
||||
Logger.log(reason)
|
||||
if (isOnline) snackString(reason)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
@@ -399,7 +426,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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -63,7 +63,7 @@ class DiscordService : Service() {
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"discordRPC:backgroundPresence"
|
||||
)
|
||||
wakeLock.acquire(30*60*1000L /*30 minutes*/)
|
||||
wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
|
||||
log("WakeLock Acquired")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val serviceChannel = NotificationChannel(
|
||||
@@ -402,7 +402,8 @@ class DiscordService : Service() {
|
||||
Thread.sleep(heartbeat.toLong())
|
||||
heartbeatSend(webSocket, sequence)
|
||||
log("WebSocket: Heartbeat Sent")
|
||||
} catch (ignored: InterruptedException) { }
|
||||
} catch (ignored: InterruptedException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -22,51 +22,81 @@ class Contributors {
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.find { it.login == "rebelonion"}?.let { first ->
|
||||
developers = developers.plus(
|
||||
Developer(
|
||||
first.login,
|
||||
first.avatarUrl,
|
||||
"Owner and Maintainer",
|
||||
first.htmlUrl
|
||||
)
|
||||
).plus(arrayOf(
|
||||
Developer(
|
||||
"Wai What",
|
||||
"https://avatars.githubusercontent.com/u/149729762?v=4",
|
||||
"Icon Designer",
|
||||
"https://github.com/WaiWhat"
|
||||
),
|
||||
Developer(
|
||||
"MarshMeadow",
|
||||
"https://avatars.githubusercontent.com/u/88599122?v=4",
|
||||
"Beta Icon Designer",
|
||||
"https://github.com/MarshMeadow?tab=repositories"
|
||||
),
|
||||
Developer(
|
||||
"Zaxx69",
|
||||
"https://avatars.githubusercontent.com/u/138523882?v=4",
|
||||
"Telegram Admin",
|
||||
"https://github.com/Zaxx69"
|
||||
),
|
||||
Developer(
|
||||
"Arif Alam",
|
||||
"https://avatars.githubusercontent.com/u/70383209?v=4",
|
||||
"Head Discord Moderator",
|
||||
"https://youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
)
|
||||
))
|
||||
}
|
||||
res.filter {it.login != "rebelonion"}.forEach {
|
||||
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,
|
||||
"Contributor",
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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
|
||||
@@ -17,10 +15,11 @@ 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")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
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(
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package ani.dantotsu.download
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
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
|
||||
@@ -12,7 +14,6 @@ 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.moveFileTo
|
||||
import com.anggrayudi.storage.file.moveFolderTo
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
@@ -20,6 +21,7 @@ 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) {
|
||||
@@ -53,10 +55,15 @@ class DownloadsManager(private val context: Context) {
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(downloadedType: DownloadedType, onFinished: () -> Unit) {
|
||||
fun removeDownload(
|
||||
downloadedType: DownloadedType,
|
||||
toast: Boolean = true,
|
||||
onFinished: () -> Unit
|
||||
) {
|
||||
removeDownloadCompat(context, downloadedType)
|
||||
downloadsList.remove(downloadedType)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
removeDirectory(downloadedType)
|
||||
removeDirectory(downloadedType, toast)
|
||||
withContext(Dispatchers.Main) {
|
||||
onFinished()
|
||||
}
|
||||
@@ -65,6 +72,7 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: MediaType) {
|
||||
removeMediaCompat(context, title, type)
|
||||
val baseDirectory = getBaseDirectory(context, type)
|
||||
val directory = baseDirectory?.findFolder(title)
|
||||
if (directory?.exists() == true) {
|
||||
@@ -80,15 +88,15 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
when (type) {
|
||||
MediaType.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA }
|
||||
}
|
||||
|
||||
MediaType.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME }
|
||||
}
|
||||
|
||||
MediaType.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL }
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
@@ -111,7 +119,7 @@ class DownloadsManager(private val context: Context) {
|
||||
if (directory?.exists() == true && directory.isDirectory) {
|
||||
val files = directory.listFiles()
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
if (!downloadsSubLists.any { it.titleName == file.name }) {
|
||||
file.deleteRecursively(context, false)
|
||||
}
|
||||
}
|
||||
@@ -120,39 +128,72 @@ class DownloadsManager(private val context: Context) {
|
||||
val iterator = downloadsList.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val download = iterator.next()
|
||||
val downloadDir = directory?.findFolder(download.title)
|
||||
if ((downloadDir?.exists() == false && 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 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 {
|
||||
|
||||
fun moveDownloadsDir(
|
||||
context: Context,
|
||||
oldUri: Uri,
|
||||
newUri: Uri,
|
||||
finished: (Boolean, String) -> Unit
|
||||
) {
|
||||
if (oldUri == newUri) {
|
||||
Logger.log("Source and destination are the same")
|
||||
finished(false, "Source and destination are the same")
|
||||
return
|
||||
}
|
||||
if (oldUri == Uri.EMPTY) {
|
||||
Logger.log("Old Uri is empty")
|
||||
finished(true, "Old Uri is empty")
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
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:
|
||||
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.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")
|
||||
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")
|
||||
@@ -163,13 +204,17 @@ class DownloadsManager(private val context: Context) {
|
||||
finished(true, "Successfully moved downloads")
|
||||
super.onCompleted(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
snackString("Error: ${e.message}")
|
||||
finished(false, "Failed to move downloads: ${e.message}")
|
||||
return
|
||||
})
|
||||
|
||||
} catch (e: Exception) {
|
||||
snackString("Error: ${e.message}")
|
||||
Logger.log("Failed to move downloads: ${e.message}")
|
||||
Logger.log(e)
|
||||
Logger.log("oldUri: $oldUri, newUri: $newUri")
|
||||
finished(false, "Failed to move downloads: ${e.message}")
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,23 +224,23 @@ class DownloadsManager(private val context: Context) {
|
||||
|
||||
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) {
|
||||
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
|
||||
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
||||
val directory =
|
||||
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
|
||||
|
||||
baseDirectory?.findFolder(downloadedType.titleName)
|
||||
?.findFolder(downloadedType.chapterName)
|
||||
downloadsList.remove(downloadedType)
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
snackString("Successfully deleted")
|
||||
|
||||
if (toast) snackString("Successfully deleted")
|
||||
} else {
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
@@ -226,11 +271,7 @@ class DownloadsManager(private val context: Context) {
|
||||
private const val MANGA_SUB_LOCATION = "Manga"
|
||||
private const val ANIME_SUB_LOCATION = "Anime"
|
||||
private const val NOVEL_SUB_LOCATION = "Novel"
|
||||
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
|
||||
|
||||
fun String?.findValidName(): String {
|
||||
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and create a base directory for the given type
|
||||
@@ -238,7 +279,6 @@ class DownloadsManager(private val context: 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
|
||||
@@ -283,7 +323,12 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getDirSize(context: Context, type: MediaType, title: String, chapter: String? = null): Long {
|
||||
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 {
|
||||
@@ -292,6 +337,19 @@ class DownloadsManager(private val context: Context) {
|
||||
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? {
|
||||
@@ -303,14 +361,38 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadedType(
|
||||
val pTitle: String, val pChapter: String, val type: MediaType
|
||||
) : Serializable {
|
||||
val title: String
|
||||
get() = pTitle.findValidName()
|
||||
val chapter: String
|
||||
get() = pChapter.findValidName()
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
@@ -32,13 +34,10 @@ 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.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import com.arthenica.ffmpegkit.SessionState
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
@@ -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 {
|
||||
@@ -163,7 +168,7 @@ class AnimeDownloaderService : Service() {
|
||||
.map { it.sessionId }.toMutableList()
|
||||
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
||||
sessionIds.forEach {
|
||||
FFmpegKit.cancel(it)
|
||||
ffExtension!!.cancelDownload(it)
|
||||
}
|
||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
@@ -198,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(
|
||||
@@ -223,66 +227,52 @@ class AnimeDownloaderService : Service() {
|
||||
) ?: throw Exception("Failed to create output directory")
|
||||
|
||||
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
|
||||
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
|
||||
val outputFile = outputDir.createFile("video/x-matroska", "${task.getTaskName()}.mkv")
|
||||
?: throw Exception("Failed to create output file")
|
||||
|
||||
var percent = 0
|
||||
var totalLength = 0.0
|
||||
val path = FFmpegKitConfig.getSafParameterForWrite(
|
||||
val path = ffExtension!!.setDownloadPath(
|
||||
this@AnimeDownloaderService,
|
||||
outputFile.uri
|
||||
)
|
||||
val headersStringBuilder = StringBuilder().append(" ")
|
||||
val headersStringBuilder = StringBuilder()
|
||||
task.video.file.headers.forEach {
|
||||
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
|
||||
}
|
||||
headersStringBuilder.append(" ")
|
||||
FFprobeKit.executeAsync(
|
||||
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\"",
|
||||
{
|
||||
Logger.log("FFprobeKit: $it")
|
||||
}, {
|
||||
if (it.message.toDoubleOrNull() != null) {
|
||||
totalLength = it.message.toDouble()
|
||||
}
|
||||
})
|
||||
|
||||
var request = "-headers"
|
||||
val headers = headersStringBuilder.toString()
|
||||
if (task.video.file.headers.isNotEmpty()) {
|
||||
request += headers
|
||||
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()
|
||||
}
|
||||
}
|
||||
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
|
||||
println("Request: $request")
|
||||
val ffTask =
|
||||
FFmpegKit.executeAsync(request,
|
||||
{ session ->
|
||||
val state: SessionState = session.state
|
||||
val returnCode = session.returnCode
|
||||
// CALLED WHEN SESSION IS EXECUTED
|
||||
Logger.log(
|
||||
java.lang.String.format(
|
||||
"FFmpeg process exited with state %s and rc %s.%s",
|
||||
state,
|
||||
returnCode,
|
||||
session.failStackTrace
|
||||
)
|
||||
)
|
||||
|
||||
}, {
|
||||
// CALLED WHEN SESSION PRINTS LOGS
|
||||
Logger.log(it.message)
|
||||
}) {
|
||||
val headers = headersStringBuilder.toString()
|
||||
var request = "-headers $headers "
|
||||
request += "-i ${task.video.file.url} -c copy -map 0:v -map 0:a -map 0:s?" +
|
||||
" -f matroska -timeout 600 -reconnect 1" +
|
||||
" -reconnect_streamed 1 -allowed_extensions ALL " +
|
||||
"-tls_verify 0 $path -v trace"
|
||||
Logger.log("Request: $request")
|
||||
val ffTask =
|
||||
ffExtension.executeFFMpeg(request) {
|
||||
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||
val timeInMilliseconds = it.time
|
||||
val timeInMilliseconds = it
|
||||
if (timeInMilliseconds > 0 && totalLength > 0) {
|
||||
percent = ((it.time / 1000) / totalLength * 100).toInt()
|
||||
percent = ((it / 1000) / totalLength * 100).toInt()
|
||||
}
|
||||
Logger.log("Statistics: $it")
|
||||
}
|
||||
task.sessionId = ffTask.sessionId
|
||||
task.sessionId = ffTask
|
||||
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
|
||||
ffTask.sessionId
|
||||
ffTask
|
||||
|
||||
saveMediaInfo(task)
|
||||
task.subtitle?.let {
|
||||
@@ -298,8 +288,8 @@ class AnimeDownloaderService : Service() {
|
||||
}
|
||||
|
||||
// periodically check if the download is complete
|
||||
while (ffTask.state != SessionState.COMPLETED) {
|
||||
if (ffTask.state == SessionState.FAILED) {
|
||||
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
||||
if (ffExtension.getState(ffTask) == "FAILED") {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
@@ -310,14 +300,15 @@ class AnimeDownloaderService : Service() {
|
||||
} Download failed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${getTaskName(task.title, task.episode)} Download failed")
|
||||
Logger.log("Download failed: ${ffTask.failStackTrace}")
|
||||
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(
|
||||
@@ -345,8 +336,8 @@ class AnimeDownloaderService : Service() {
|
||||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
if (ffTask.state == SessionState.COMPLETED) {
|
||||
if (ffTask.returnCode.isValueError) {
|
||||
if (ffExtension.getState(ffTask) == "COMPLETED") {
|
||||
if (ffExtension.hadError(ffTask)) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -31,9 +30,13 @@ 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.findValidName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.findValidName
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
@@ -88,9 +91,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
val color = requireContext().getThemeColor(android.R.attr.windowBackground)
|
||||
|
||||
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||
animeUserAvatar.setSafeOnClickListener {
|
||||
@@ -175,7 +176,7 @@ 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.findValidName() }
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
media?.let {
|
||||
lifecycleScope.launch {
|
||||
val mediaModel = getMedia(it)
|
||||
@@ -287,11 +288,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
}
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val download = tDownloads.firstOrNull() ?: continue
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
}
|
||||
@@ -313,7 +314,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
return try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
@@ -327,7 +328,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
})
|
||||
.create()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return null
|
||||
?: return loadMediaCompat(downloadedType)
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
@@ -352,7 +353,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
@@ -363,6 +364,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
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()
|
||||
@@ -391,22 +393,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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -28,9 +27,14 @@ 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.getThemeColor
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
@@ -81,9 +85,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
val color = requireContext().getThemeColor(android.R.attr.windowBackground)
|
||||
|
||||
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||
animeUserAvatar.setSafeOnClickListener {
|
||||
@@ -168,8 +170,8 @@ 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 {
|
||||
lifecycleScope.launch {
|
||||
ContextCompat.startActivity(
|
||||
@@ -189,7 +191,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: MediaType =
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) {
|
||||
MediaType.MANGA
|
||||
} else {
|
||||
MediaType.NOVEL
|
||||
@@ -277,20 +279,20 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
downloads = listOf()
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val download = tDownloads.firstOrNull() ?: continue
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val download = tDownloads.firstOrNull() ?: continue
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
}
|
||||
@@ -314,7 +316,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
return try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
@@ -322,7 +324,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
})
|
||||
.create()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return null
|
||||
?: return DownloadCompat.loadMediaCompat(downloadedType)
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
@@ -342,7 +344,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
@@ -353,6 +355,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
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()
|
||||
@@ -375,21 +378,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,7 +9,6 @@ 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
|
||||
@@ -50,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
|
||||
@@ -270,7 +267,9 @@ class NovelDownloaderService : Service() {
|
||||
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 outputStream =
|
||||
this@NovelDownloaderService.contentResolver.openOutputStream(file.uri)
|
||||
?: throw Exception("Could not open OutputStream")
|
||||
|
||||
val sink = outputStream.sink().buffer()
|
||||
val responseBody = response.body
|
||||
@@ -358,7 +357,7 @@ class NovelDownloaderService : Service() {
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
launchIO {
|
||||
val directory =
|
||||
DownloadsManager.getSubDirectory(
|
||||
getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
|
||||
@@ -7,14 +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 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
|
||||
@@ -22,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
|
||||
@@ -34,21 +27,16 @@ 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.media.MediaType
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import 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.Executors
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@@ -130,4 +118,92 @@ object Helper {
|
||||
}
|
||||
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>()
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val okHttpClient = networkHelper.client
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
dataSource
|
||||
}
|
||||
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||
val downloadManager = DownloadManager(
|
||||
context,
|
||||
database,
|
||||
getSimpleCache(context),
|
||||
dataSourceFactory,
|
||||
executorService
|
||||
).apply {
|
||||
requirements =
|
||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
maxParallelDownloads = 3
|
||||
}
|
||||
downloadManager.addListener( //for testing
|
||||
object : DownloadManager.Listener {
|
||||
override fun onDownloadChanged(
|
||||
downloadManager: DownloadManager,
|
||||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
Logger.log("Download Completed")
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
Logger.log("Download Failed")
|
||||
} else if (download.state == Download.STATE_STOPPED) {
|
||||
Logger.log("Download Stopped")
|
||||
} else if (download.state == Download.STATE_QUEUED) {
|
||||
Logger.log("Download Queued")
|
||||
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||
Logger.log("Download Downloading")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
downloadManager
|
||||
}
|
||||
}
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
@OptIn(UnstableApi::class)
|
||||
fun getSimpleCache(context: Context): SimpleCache {
|
||||
return if (simpleCache == null) {
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
|
||||
simpleCache!!
|
||||
} else {
|
||||
simpleCache!!
|
||||
}
|
||||
}
|
||||
@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 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
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -204,22 +205,22 @@ class AnimeFragment : Fragment() {
|
||||
if (i) {
|
||||
model.getUpdated().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
|
||||
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()), it)
|
||||
}
|
||||
}
|
||||
model.getMovies().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()))
|
||||
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()), it)
|
||||
}
|
||||
}
|
||||
model.getTopRated().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
|
||||
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()), it)
|
||||
}
|
||||
}
|
||||
model.getMostFav().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
|
||||
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()), it)
|
||||
}
|
||||
}
|
||||
if (animePageAdapter.trendingViewPager != null) {
|
||||
@@ -268,13 +269,25 @@ class AnimeFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
|
||||
var running = false
|
||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(false) }
|
||||
live.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
if (it && !running) {
|
||||
running = true
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
|
||||
?.toIntOrNull()
|
||||
if (Anilist.userid == null) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending(1)
|
||||
@@ -287,6 +300,7 @@ class AnimeFragment : Fragment() {
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.animeRefresh?.isRefreshing = false
|
||||
running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package ani.dantotsu.home
|
||||
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
|
||||
@@ -14,6 +13,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -24,10 +24,14 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.CalendarActivity
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.MediaListViewActivity
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.px
|
||||
@@ -67,10 +71,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
val materialCardView =
|
||||
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
|
||||
|
||||
val color = binding.root.context.getThemeColor(android.R.attr.windowBackground)
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
@@ -101,7 +102,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
ContextCompat.startActivity(
|
||||
view.context,
|
||||
Intent(view.context, ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid),null
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
@@ -110,7 +111,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
trendingBinding.searchBar.performClick()
|
||||
}
|
||||
|
||||
trendingBinding.notificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
|
||||
listOf(
|
||||
@@ -167,7 +169,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
trendingBinding.trendingProgressBar.visibility = View.GONE
|
||||
trendingBinding.trendingViewPager.adapter = adaptor
|
||||
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
||||
RecyclerView.OVER_SCROLL_NEVER
|
||||
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
@@ -194,13 +197,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateRecent(adaptor: MediaAdaptor) {
|
||||
binding.apply{
|
||||
fun updateRecent(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeUpdatedRecyclerView,
|
||||
animeUpdatedProgressBar,
|
||||
animeRecently
|
||||
animeRecently,
|
||||
animeRecentlyMore,
|
||||
getAppString(R.string.updated),
|
||||
media
|
||||
)
|
||||
animePopular.visibility = View.VISIBLE
|
||||
animePopular.startAnimation(setSlideUp())
|
||||
@@ -210,38 +216,50 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
}
|
||||
fun updateMovies(adaptor: MediaAdaptor) {
|
||||
binding.apply{
|
||||
|
||||
fun updateMovies(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeMoviesRecyclerView,
|
||||
animeMoviesProgressBar,
|
||||
animeMovies
|
||||
animeMovies,
|
||||
animeMoviesMore,
|
||||
getAppString(R.string.trending_movies),
|
||||
media
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply{
|
||||
fun updateTopRated(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeTopRatedRecyclerView,
|
||||
animeTopRatedProgressBar,
|
||||
animeTopRated
|
||||
animeTopRated,
|
||||
animeTopRatedMore,
|
||||
getAppString(R.string.top_rated),
|
||||
media
|
||||
)
|
||||
}
|
||||
}
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply{
|
||||
|
||||
fun updateMostFav(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeMostFavRecyclerView,
|
||||
animeMostFavProgressBar,
|
||||
animeMostFav
|
||||
animeMostFav,
|
||||
animeMostFavMore,
|
||||
getAppString(R.string.most_favourite),
|
||||
media
|
||||
)
|
||||
}
|
||||
}
|
||||
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
|
||||
|
||||
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View , more: View , string: String, media : MutableList<Media>) {
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
@@ -250,12 +268,23 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
more.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
it.context, Intent(it.context, MediaListViewActivity::class.java)
|
||||
.putExtra("title", string)
|
||||
.putExtra("media", media as ArrayList<Media>),
|
||||
null
|
||||
)
|
||||
}
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
more.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp())
|
||||
more.startAnimation(setSlideUp())
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
if (Anilist.avatar != null && ready.value == true) {
|
||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||
|
||||
@@ -30,9 +30,11 @@ import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.FragmentHomeBinding
|
||||
import ani.dantotsu.home.status.UserStatusAdapter
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.MediaListViewActivity
|
||||
import ani.dantotsu.media.user.ListActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
@@ -41,12 +43,14 @@ import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -55,7 +59,6 @@ import kotlin.math.min
|
||||
class HomeFragment : Fragment() {
|
||||
private var _binding: FragmentHomeBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -74,14 +77,19 @@ class HomeFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val scope = lifecycleScope
|
||||
Logger.log("HomeFragment")
|
||||
fun load() {
|
||||
Logger.log("Loading HomeFragment")
|
||||
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
|
||||
binding.homeUserName.text = Anilist.username
|
||||
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
||||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
blurImage(if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen, Anilist.bg)
|
||||
blurImage(
|
||||
if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen,
|
||||
Anilist.bg
|
||||
)
|
||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
@@ -128,7 +136,7 @@ class HomeFragment : Fragment() {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
ContextCompat.startActivity(
|
||||
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid),null
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
@@ -202,13 +210,16 @@ class HomeFragment : Fragment() {
|
||||
recyclerView: RecyclerView,
|
||||
progress: View,
|
||||
empty: View,
|
||||
title: View
|
||||
title: View,
|
||||
more: View,
|
||||
string: String
|
||||
) {
|
||||
container.visibility = View.VISIBLE
|
||||
progress.visibility = View.VISIBLE
|
||||
recyclerView.visibility = View.GONE
|
||||
empty.visibility = View.GONE
|
||||
title.visibility = View.INVISIBLE
|
||||
more.visibility = View.INVISIBLE
|
||||
|
||||
mode.observe(viewLifecycleOwner) {
|
||||
recyclerView.visibility = View.GONE
|
||||
@@ -221,6 +232,14 @@ class HomeFragment : Fragment() {
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
more.setOnClickListener { i ->
|
||||
ContextCompat.startActivity(
|
||||
i.context, Intent(i.context, MediaListViewActivity::class.java)
|
||||
.putExtra("title", string)
|
||||
.putExtra("media", it),
|
||||
null
|
||||
)
|
||||
}
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
@@ -228,7 +247,9 @@ class HomeFragment : Fragment() {
|
||||
} else {
|
||||
empty.visibility = View.VISIBLE
|
||||
}
|
||||
more.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
more.startAnimation(setSlideUp())
|
||||
title.startAnimation(setSlideUp())
|
||||
progress.visibility = View.GONE
|
||||
}
|
||||
@@ -243,7 +264,9 @@ class HomeFragment : Fragment() {
|
||||
binding.homeWatchingRecyclerView,
|
||||
binding.homeWatchingProgressBar,
|
||||
binding.homeWatchingEmpty,
|
||||
binding.homeContinueWatch
|
||||
binding.homeContinueWatch,
|
||||
binding.homeContinueWatchMore,
|
||||
getString(R.string.continue_watching)
|
||||
)
|
||||
binding.homeWatchingBrowseButton.setOnClickListener {
|
||||
bottomBar.selectTabAt(0)
|
||||
@@ -255,7 +278,9 @@ class HomeFragment : Fragment() {
|
||||
binding.homeFavAnimeRecyclerView,
|
||||
binding.homeFavAnimeProgressBar,
|
||||
binding.homeFavAnimeEmpty,
|
||||
binding.homeFavAnime
|
||||
binding.homeFavAnime,
|
||||
binding.homeFavAnimeMore,
|
||||
getString(R.string.fav_anime)
|
||||
)
|
||||
|
||||
initRecyclerView(
|
||||
@@ -264,7 +289,9 @@ class HomeFragment : Fragment() {
|
||||
binding.homePlannedAnimeRecyclerView,
|
||||
binding.homePlannedAnimeProgressBar,
|
||||
binding.homePlannedAnimeEmpty,
|
||||
binding.homePlannedAnime
|
||||
binding.homePlannedAnime,
|
||||
binding.homePlannedAnimeMore,
|
||||
getString(R.string.planned_anime)
|
||||
)
|
||||
binding.homePlannedAnimeBrowseButton.setOnClickListener {
|
||||
bottomBar.selectTabAt(0)
|
||||
@@ -276,7 +303,9 @@ class HomeFragment : Fragment() {
|
||||
binding.homeReadingRecyclerView,
|
||||
binding.homeReadingProgressBar,
|
||||
binding.homeReadingEmpty,
|
||||
binding.homeContinueRead
|
||||
binding.homeContinueRead,
|
||||
binding.homeContinueReadMore,
|
||||
getString(R.string.continue_reading)
|
||||
)
|
||||
binding.homeReadingBrowseButton.setOnClickListener {
|
||||
bottomBar.selectTabAt(2)
|
||||
@@ -288,7 +317,9 @@ class HomeFragment : Fragment() {
|
||||
binding.homeFavMangaRecyclerView,
|
||||
binding.homeFavMangaProgressBar,
|
||||
binding.homeFavMangaEmpty,
|
||||
binding.homeFavManga
|
||||
binding.homeFavManga,
|
||||
binding.homeFavMangaMore,
|
||||
getString(R.string.fav_manga)
|
||||
)
|
||||
|
||||
initRecyclerView(
|
||||
@@ -297,7 +328,9 @@ class HomeFragment : Fragment() {
|
||||
binding.homePlannedMangaRecyclerView,
|
||||
binding.homePlannedMangaProgressBar,
|
||||
binding.homePlannedMangaEmpty,
|
||||
binding.homePlannedManga
|
||||
binding.homePlannedManga,
|
||||
binding.homePlannedMangaMore,
|
||||
getString(R.string.planned_manga)
|
||||
)
|
||||
binding.homePlannedMangaBrowseButton.setOnClickListener {
|
||||
bottomBar.selectTabAt(2)
|
||||
@@ -309,12 +342,87 @@ class HomeFragment : Fragment() {
|
||||
binding.homeRecommendedRecyclerView,
|
||||
binding.homeRecommendedProgressBar,
|
||||
binding.homeRecommendedEmpty,
|
||||
binding.homeRecommended
|
||||
binding.homeRecommended,
|
||||
binding.homeRecommendedMore,
|
||||
getString(R.string.recommended)
|
||||
)
|
||||
binding.homeUserStatusContainer.visibility = View.VISIBLE
|
||||
binding.homeUserStatusProgressBar.visibility = View.VISIBLE
|
||||
binding.homeUserStatusRecyclerView.visibility = View.GONE
|
||||
model.getUserStatus().observe(viewLifecycleOwner) {
|
||||
binding.homeUserStatusRecyclerView.visibility = View.GONE
|
||||
if (it != null) {
|
||||
if (it.isNotEmpty()) {
|
||||
PrefManager.getLiveVal(PrefName.RefreshStatus, false).apply {
|
||||
asLiveBool()
|
||||
observe(viewLifecycleOwner) { _ ->
|
||||
binding.homeUserStatusRecyclerView.adapter = UserStatusAdapter(it)
|
||||
}
|
||||
}
|
||||
binding.homeUserStatusRecyclerView.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.homeUserStatusRecyclerView.visibility = View.VISIBLE
|
||||
binding.homeUserStatusRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
|
||||
} else {
|
||||
binding.homeUserStatusContainer.visibility = View.GONE
|
||||
}
|
||||
binding.homeUserStatusProgressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
}
|
||||
binding.homeHiddenItemsContainer.visibility = View.GONE
|
||||
model.getHidden().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
if (it.isNotEmpty()) {
|
||||
binding.homeHiddenItemsRecyclerView.adapter =
|
||||
MediaAdaptor(0, it, requireActivity())
|
||||
binding.homeHiddenItemsRecyclerView.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.homeContinueWatch.setOnLongClickListener {
|
||||
binding.homeHiddenItemsContainer.visibility = View.VISIBLE
|
||||
binding.homeHiddenItemsRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
true
|
||||
}
|
||||
binding.homeHiddenItemsMore.setSafeOnClickListener { _ ->
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireActivity(), MediaListViewActivity::class.java)
|
||||
.putExtra("title", getString(R.string.hidden))
|
||||
.putExtra("media", it),
|
||||
null
|
||||
)
|
||||
}
|
||||
binding.homeHiddenItemsTitle.setOnLongClickListener {
|
||||
binding.homeHiddenItemsContainer.visibility = View.GONE
|
||||
true
|
||||
}
|
||||
} else {
|
||||
binding.homeContinueWatch.setOnLongClickListener {
|
||||
snackString(getString(R.string.no_hidden_items))
|
||||
true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.homeContinueWatch.setOnLongClickListener {
|
||||
snackString(getString(R.string.no_hidden_items))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||
|
||||
model.empty.observe(viewLifecycleOwner) {
|
||||
model.empty.observe(viewLifecycleOwner)
|
||||
{
|
||||
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
|
||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||
binding.homeDantotsuContainer.startAnimation(setSlideUp())
|
||||
@@ -331,7 +439,8 @@ class HomeFragment : Fragment() {
|
||||
"MangaContinue",
|
||||
"MangaFav",
|
||||
"MangaPlanned",
|
||||
"Recommendation"
|
||||
"Recommendation",
|
||||
"UserStatus",
|
||||
)
|
||||
|
||||
val containers = arrayOf(
|
||||
@@ -341,26 +450,41 @@ class HomeFragment : Fragment() {
|
||||
binding.homeContinueReadingContainer,
|
||||
binding.homeFavMangaContainer,
|
||||
binding.homePlannedMangaContainer,
|
||||
binding.homeRecommendedContainer
|
||||
binding.homeRecommendedContainer,
|
||||
binding.homeUserStatusContainer,
|
||||
)
|
||||
|
||||
val live = Refresh.activity.getOrPut(1) { MutableLiveData(false) }
|
||||
live.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
var running = false
|
||||
val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) }
|
||||
live.observe(viewLifecycleOwner)
|
||||
{
|
||||
if (!running && it) {
|
||||
running = true
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
//Get userData First
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
Anilist.userid =
|
||||
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
|
||||
?.toIntOrNull()
|
||||
if (Anilist.userid == null) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
model.loaded = true
|
||||
model.setListImages()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
model.setListImages()
|
||||
}
|
||||
var empty = true
|
||||
val homeLayoutShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayoutShow)
|
||||
runBlocking {
|
||||
model.initHomePage()
|
||||
}
|
||||
PrefManager.getVal(PrefName.HomeLayout)
|
||||
model.initHomePage()
|
||||
(array.indices).forEach { i ->
|
||||
if (homeLayoutShow.elementAt(i)) {
|
||||
empty = false
|
||||
@@ -372,10 +496,14 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.homeRefresh?.isRefreshing = false
|
||||
running = false
|
||||
}
|
||||
binding.homeHiddenItemsContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
|
||||
if (_binding != null) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -162,27 +163,33 @@ class MangaFragment : Fragment() {
|
||||
if (i == true) {
|
||||
model.getPopularNovel().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
|
||||
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()), it)
|
||||
}
|
||||
}
|
||||
model.getPopularManga().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()))
|
||||
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()), it)
|
||||
}
|
||||
}
|
||||
model.getPopularManhwa().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrendingManhwa(MediaAdaptor(0, it, requireActivity()))
|
||||
mangaPageAdapter.updateTrendingManhwa(
|
||||
MediaAdaptor(
|
||||
0,
|
||||
it,
|
||||
requireActivity()
|
||||
), it
|
||||
)
|
||||
}
|
||||
}
|
||||
model.getTopRated().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
|
||||
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()), it)
|
||||
}
|
||||
}
|
||||
model.getMostFav().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
|
||||
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()), it)
|
||||
}
|
||||
}
|
||||
if (mangaPageAdapter.trendingViewPager != null) {
|
||||
@@ -247,13 +254,25 @@ class MangaFragment : Fragment() {
|
||||
mangaPageAdapter.updateAvatar()
|
||||
}
|
||||
|
||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(false) }
|
||||
var running = false
|
||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
||||
live.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
if (!running && it) {
|
||||
running = true
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
|
||||
?.toIntOrNull()
|
||||
if (Anilist.userid == null) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending()
|
||||
@@ -266,6 +285,7 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.mangaRefresh?.isRefreshing = false
|
||||
running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package ani.dantotsu.home
|
||||
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
|
||||
@@ -21,12 +20,15 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.MediaPageTransformer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.MediaListViewActivity
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.px
|
||||
@@ -66,10 +68,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
val materialCardView =
|
||||
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
|
||||
|
||||
val color = binding.root.context.getThemeColor(android.R.attr.windowBackground)
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
@@ -101,7 +100,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
ContextCompat.startActivity(
|
||||
view.context,
|
||||
Intent(view.context, ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid),null
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
@@ -156,7 +155,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
trendingBinding.trendingProgressBar.visibility = View.GONE
|
||||
trendingBinding.trendingViewPager.adapter = adaptor
|
||||
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
||||
RecyclerView.OVER_SCROLL_NEVER
|
||||
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
@@ -181,60 +181,87 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
|
||||
}
|
||||
|
||||
fun updateTrendingManga(adaptor: MediaAdaptor) {
|
||||
fun updateTrendingManga(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTrendingMangaRecyclerView,
|
||||
mangaTrendingMangaProgressBar,
|
||||
mangaTrendingManga
|
||||
mangaTrendingManga,
|
||||
mangaTrendingMangaMore,
|
||||
getAppString(R.string.trending_manga),
|
||||
media
|
||||
)
|
||||
}
|
||||
}
|
||||
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
|
||||
|
||||
fun updateTrendingManhwa(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTrendingManhwaRecyclerView,
|
||||
mangaTrendingManhwaProgressBar,
|
||||
mangaTrendingManhwa
|
||||
mangaTrendingManhwa,
|
||||
mangaTrendingManhwaMore,
|
||||
getAppString(R.string.trending_manhwa),
|
||||
media
|
||||
)
|
||||
}
|
||||
}
|
||||
fun updateNovel(adaptor: MediaAdaptor) {
|
||||
|
||||
fun updateNovel(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaNovelRecyclerView,
|
||||
mangaNovelProgressBar,
|
||||
mangaNovel
|
||||
mangaNovel,
|
||||
mangaNovelMore,
|
||||
getAppString(R.string.trending_novel),
|
||||
media
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTopRatedRecyclerView,
|
||||
mangaTopRatedProgressBar,
|
||||
mangaTopRated
|
||||
mangaTopRated,
|
||||
mangaTopRatedMore,
|
||||
getAppString(R.string.top_rated),
|
||||
media
|
||||
)
|
||||
}
|
||||
}
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
|
||||
fun updateMostFav(adaptor: MediaAdaptor, media: MutableList<Media>) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaMostFavRecyclerView,
|
||||
mangaMostFavProgressBar,
|
||||
mangaMostFav
|
||||
mangaMostFav,
|
||||
mangaMostFavMore,
|
||||
getAppString(R.string.most_favourite),
|
||||
media
|
||||
)
|
||||
mangaPopular.visibility = View.VISIBLE
|
||||
mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
}
|
||||
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
|
||||
|
||||
fun init(
|
||||
adaptor: MediaAdaptor,
|
||||
recyclerView: RecyclerView,
|
||||
progress: View,
|
||||
title: View ,
|
||||
more: View ,
|
||||
string: String,
|
||||
media : MutableList<Media>
|
||||
) {
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
@@ -243,9 +270,19 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
more.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
it.context, Intent(it.context, MediaListViewActivity::class.java)
|
||||
.putExtra("title", string)
|
||||
.putExtra("media", media as ArrayList<Media>),
|
||||
null
|
||||
)
|
||||
}
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
more.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp())
|
||||
more.startAnimation(setSlideUp())
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
82
app/src/main/java/ani/dantotsu/home/status/CircleView.kt
Normal file
82
app/src/main/java/ani/dantotsu/home/status/CircleView.kt
Normal file
@@ -0,0 +1,82 @@
|
||||
package ani.dantotsu.home.status
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import ani.dantotsu.getThemeColor
|
||||
|
||||
class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
|
||||
private var parts: Int = 3
|
||||
private var gapAngle: Float = 9f
|
||||
private val path = Path()
|
||||
private var isUser = false
|
||||
private var booleanList = listOf<Boolean>()
|
||||
private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 6f
|
||||
}
|
||||
|
||||
@SuppressLint("DrawAllocation")
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val centerX = width / 2f
|
||||
val centerY = height / 2f
|
||||
val radius = centerX.coerceAtMost(centerY) - paint.strokeWidth / 2
|
||||
|
||||
val totalGapAngle = gapAngle * (parts)
|
||||
val totalAngle = 360f - totalGapAngle
|
||||
|
||||
|
||||
val primaryColor = context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
|
||||
val secondColor = context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary)
|
||||
|
||||
fun setColor(int: Int) {
|
||||
paint.color = if (int < booleanList.size && booleanList[int]) {
|
||||
Color.GRAY
|
||||
} else {
|
||||
if (isUser) secondColor else primaryColor
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
|
||||
if (parts == 1) {
|
||||
path.addArc(
|
||||
centerX - radius,
|
||||
centerY - radius,
|
||||
centerX + radius,
|
||||
centerY + radius,
|
||||
0f,
|
||||
360f
|
||||
)
|
||||
setColor(0)
|
||||
} else {
|
||||
val effectiveAngle = totalAngle / parts
|
||||
for (i in 0 until parts) {
|
||||
val startAngle = i * (effectiveAngle + gapAngle) -90f
|
||||
path.reset()
|
||||
path.addArc(
|
||||
centerX - radius,
|
||||
centerY - radius,
|
||||
centerX + radius,
|
||||
centerY + radius,
|
||||
startAngle,
|
||||
effectiveAngle
|
||||
)
|
||||
setColor(i)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun setParts(parts: Int, list : List<Boolean> = mutableListOf(), isUser: Boolean) {
|
||||
this.parts = parts
|
||||
this.booleanList = list
|
||||
this.isUser = isUser
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
110
app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt
Normal file
110
app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt
Normal file
@@ -0,0 +1,110 @@
|
||||
package ani.dantotsu.home.status
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.api.Activity
|
||||
import ani.dantotsu.databinding.ActivityStatusBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.home.status.listener.StoriesCallback
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.statusBarHeight
|
||||
|
||||
class StatusActivity : AppCompatActivity(), StoriesCallback {
|
||||
private lateinit var activity: ArrayList<User>
|
||||
private lateinit var binding: ActivityStatusBinding
|
||||
private var position: Int = -1
|
||||
private lateinit var slideInLeft: Animation
|
||||
private lateinit var slideOutRight: Animation
|
||||
private lateinit var slideOutLeft: Animation
|
||||
private lateinit var slideInRight: Animation
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
initActivity(this)
|
||||
binding = ActivityStatusBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
activity = user
|
||||
position = intent.getIntExtra("position", -1)
|
||||
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
slideInLeft = AnimationUtils.loadAnimation(this, R.anim.slide_in_left)
|
||||
slideOutRight = AnimationUtils.loadAnimation(this, R.anim.slide_out_right)
|
||||
slideOutLeft = AnimationUtils.loadAnimation(this, R.anim.slide_out_left)
|
||||
slideInRight = AnimationUtils.loadAnimation(this, R.anim.slide_in_right)
|
||||
|
||||
val key = "activities"
|
||||
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
|
||||
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
|
||||
val startIndex = if ( startFrom > 0) startFrom else 0
|
||||
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
|
||||
|
||||
|
||||
}
|
||||
private fun findFirstNonMatch(watchedActivity: Set<Int>, activity: List<Activity>): Int {
|
||||
for (activityItem in activity) {
|
||||
if (activityItem.id !in watchedActivity) {
|
||||
return activity.indexOf(activityItem)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
binding.stories.pause()
|
||||
}
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.stories.resume()
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
binding.stories.resume()
|
||||
} else {
|
||||
binding.stories.pause()
|
||||
}
|
||||
}
|
||||
override fun onStoriesEnd() {
|
||||
position += 1
|
||||
if (position < activity.size) {
|
||||
val key = "activities"
|
||||
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
|
||||
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
|
||||
val startIndex= if ( startFrom > 0) startFrom else 0
|
||||
binding.stories.startAnimation(slideOutLeft)
|
||||
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
|
||||
binding.stories.startAnimation(slideInRight)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStoriesStart() {
|
||||
position -= 1
|
||||
if (position >= 0) {
|
||||
val key = "activities"
|
||||
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
|
||||
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
|
||||
val startIndex = if ( startFrom > 0) startFrom else 0
|
||||
binding.stories.startAnimation(slideOutRight)
|
||||
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
|
||||
binding.stories.startAnimation(slideInLeft)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
var user: ArrayList<User> = arrayListOf()
|
||||
}
|
||||
}
|
||||
491
app/src/main/java/ani/dantotsu/home/status/Stories.kt
Normal file
491
app/src/main/java/ani/dantotsu/home/status/Stories.kt
Normal file
@@ -0,0 +1,491 @@
|
||||
package ani.dantotsu.home.status
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.buildMarkwon
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.Activity
|
||||
import ani.dantotsu.databinding.FragmentStatusBinding
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.home.status.listener.StoriesCallback
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.profile.UsersDialogFragment
|
||||
import ani.dantotsu.profile.activity.ActivityItemBuilder
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.AniMarkdown
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class Stories @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
|
||||
private lateinit var activity: FragmentActivity
|
||||
private lateinit var binding: FragmentStatusBinding
|
||||
private lateinit var activityList: List<Activity>
|
||||
private lateinit var storiesListener: StoriesCallback
|
||||
private var userClicked: Boolean = false
|
||||
private var storyIndex: Int = 1
|
||||
private var primaryColor: Int = 0
|
||||
private var onPrimaryColor: Int = 0
|
||||
private var storyDuration: Int = 6
|
||||
private val timer: StoryTimer = StoryTimer(secondsToMillis(storyDuration))
|
||||
|
||||
init {
|
||||
initLayout()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun initLayout() {
|
||||
val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
binding = FragmentStatusBinding.inflate(inflater, this, false)
|
||||
addView(binding.root)
|
||||
|
||||
primaryColor = context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
|
||||
onPrimaryColor = context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary)
|
||||
|
||||
if (context is StoriesCallback) storiesListener = context as StoriesCallback
|
||||
|
||||
binding.leftTouchPanel.setOnTouchListener(this)
|
||||
binding.rightTouchPanel.setOnTouchListener(this)
|
||||
}
|
||||
|
||||
|
||||
fun setStoriesList(
|
||||
activityList: List<Activity>, activity: FragmentActivity, startIndex: Int = 1
|
||||
) {
|
||||
this.activityList = activityList
|
||||
this.activity = activity
|
||||
this.storyIndex = startIndex
|
||||
addLoadingViews(activityList)
|
||||
}
|
||||
|
||||
private fun addLoadingViews(storiesList: List<Activity>) {
|
||||
var idCounter = 1
|
||||
for (story in storiesList) {
|
||||
binding.progressBarContainer.removeView(findViewWithTag<ProgressBar>("story${idCounter}"))
|
||||
val progressBar = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal)
|
||||
progressBar.visibility = View.VISIBLE
|
||||
progressBar.id = idCounter
|
||||
progressBar.tag = "story${idCounter++}"
|
||||
progressBar.progressBackgroundTintList = ColorStateList.valueOf(primaryColor)
|
||||
progressBar.progressTintList = ColorStateList.valueOf(onPrimaryColor)
|
||||
val params = LayoutParams(0, LayoutParams.WRAP_CONTENT)
|
||||
params.marginEnd = 5
|
||||
params.marginStart = 5
|
||||
binding.progressBarContainer.addView(progressBar, params)
|
||||
}
|
||||
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(binding.progressBarContainer)
|
||||
|
||||
var counter = storiesList.size
|
||||
for (story in storiesList) {
|
||||
val progressBar = findViewWithTag<ProgressBar>("story${counter}")
|
||||
if (progressBar != null) {
|
||||
if (storiesList.size > 1) {
|
||||
when (counter) {
|
||||
storiesList.size -> {
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.END,
|
||||
LayoutParams.PARENT_ID,
|
||||
ConstraintSet.END
|
||||
)
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.TOP,
|
||||
LayoutParams.PARENT_ID,
|
||||
ConstraintSet.TOP
|
||||
)
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.START,
|
||||
getId("story${counter - 1}"),
|
||||
ConstraintSet.END
|
||||
)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.TOP,
|
||||
LayoutParams.PARENT_ID,
|
||||
ConstraintSet.TOP
|
||||
)
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.START,
|
||||
LayoutParams.PARENT_ID,
|
||||
ConstraintSet.START
|
||||
)
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.END,
|
||||
getId("story${counter + 1}"),
|
||||
ConstraintSet.START
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.TOP,
|
||||
LayoutParams.PARENT_ID,
|
||||
ConstraintSet.TOP
|
||||
)
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.START,
|
||||
getId("story${counter - 1}"),
|
||||
ConstraintSet.END
|
||||
)
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.END,
|
||||
getId("story${counter + 1}"),
|
||||
ConstraintSet.START
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.END,
|
||||
LayoutParams.PARENT_ID,
|
||||
ConstraintSet.END
|
||||
)
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.TOP,
|
||||
LayoutParams.PARENT_ID,
|
||||
ConstraintSet.TOP
|
||||
)
|
||||
constraintSet.connect(
|
||||
getId("story${counter}"),
|
||||
ConstraintSet.START,
|
||||
LayoutParams.PARENT_ID,
|
||||
ConstraintSet.START
|
||||
)
|
||||
}
|
||||
}
|
||||
counter--
|
||||
}
|
||||
constraintSet.applyTo(binding.progressBarContainer)
|
||||
startShowContent()
|
||||
}
|
||||
|
||||
private fun startShowContent() {
|
||||
showStory()
|
||||
}
|
||||
|
||||
private fun showStory() {
|
||||
if (storyIndex > 1) {
|
||||
completeProgressBar(storyIndex - 1)
|
||||
}
|
||||
val progressBar = findViewWithTag<ProgressBar>("story${storyIndex}")
|
||||
binding.androidStoriesLoadingView.visibility = View.VISIBLE
|
||||
timer.setOnTimerCompletedListener {
|
||||
Logger.log("onAnimationEnd: $storyIndex")
|
||||
if (storyIndex - 1 <= activityList.size) {
|
||||
if (false) {
|
||||
Logger.log("userClicked: $storyIndex")
|
||||
userClicked = false
|
||||
} else {
|
||||
Logger.log("userNotClicked: $storyIndex")
|
||||
if (storyIndex < activityList.size) {
|
||||
storyIndex += 1
|
||||
showStory()
|
||||
} else {
|
||||
// on stories end
|
||||
binding.androidStoriesLoadingView.visibility = View.GONE
|
||||
onStoriesCompleted()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// on stories end
|
||||
binding.androidStoriesLoadingView.visibility = View.GONE
|
||||
onStoriesCompleted()
|
||||
}
|
||||
}
|
||||
timer.setOnPercentTickListener {
|
||||
progressBar.progress = it
|
||||
}
|
||||
loadStory(activityList[storyIndex - 1])
|
||||
}
|
||||
|
||||
private fun getId(tag: String): Int {
|
||||
return findViewWithTag<ProgressBar>(tag).id
|
||||
}
|
||||
|
||||
private fun secondsToMillis(seconds: Int): Long {
|
||||
return (seconds.toLong()).times(1000)
|
||||
}
|
||||
|
||||
private fun resetProgressBar(storyIndex: Int) {
|
||||
for (i in storyIndex until activityList.size + 1) {
|
||||
val progressBar = findViewWithTag<ProgressBar>("story${i}")
|
||||
progressBar?.let {
|
||||
it.progress = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeProgressBar(storyIndex: Int) {
|
||||
for (i in 1 until storyIndex + 1) {
|
||||
val progressBar = findViewWithTag<ProgressBar>("story${i}")
|
||||
progressBar?.let {
|
||||
it.progress = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var startClickTime = 0L
|
||||
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||
val maxClickDuration = 200
|
||||
when (event?.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startClickTime = Calendar.getInstance().timeInMillis
|
||||
pause()
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
|
||||
if (clickDuration < maxClickDuration) {
|
||||
//click occurred
|
||||
view?.let {
|
||||
if (it.id == R.id.leftTouchPanel) {
|
||||
leftPanelTouch()
|
||||
} else if (it.id == R.id.rightTouchPanel) {
|
||||
rightPanelTouch()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//hold click occurred
|
||||
resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun rightPanelTouch() {
|
||||
Logger.log("rightPanelTouch: $storyIndex")
|
||||
if (storyIndex == activityList.size) {
|
||||
completeProgressBar(storyIndex)
|
||||
onStoriesCompleted()
|
||||
return
|
||||
}
|
||||
userClicked = true
|
||||
timer.cancel()
|
||||
if (storyIndex <= activityList.size) storyIndex += 1
|
||||
showStory()
|
||||
}
|
||||
|
||||
private fun leftPanelTouch() {
|
||||
Logger.log("leftPanelTouch: $storyIndex")
|
||||
if (storyIndex == 1) {
|
||||
onStoriesPrevious()
|
||||
return
|
||||
}
|
||||
userClicked = true
|
||||
timer.cancel()
|
||||
resetProgressBar(storyIndex)
|
||||
if (storyIndex > 1) storyIndex -= 1
|
||||
showStory()
|
||||
}
|
||||
|
||||
private fun onStoriesCompleted() {
|
||||
Logger.log("onStoriesCompleted")
|
||||
if (::storiesListener.isInitialized) {
|
||||
storyIndex = 1
|
||||
storiesListener.onStoriesEnd()
|
||||
resetProgressBar(storyIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStoriesPrevious() {
|
||||
if (::storiesListener.isInitialized) {
|
||||
storyIndex = 1
|
||||
storiesListener.onStoriesStart()
|
||||
resetProgressBar(storyIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
timer.pause()
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
timer.resume()
|
||||
}
|
||||
|
||||
private fun loadStory(story: Activity) {
|
||||
val key = "activities"
|
||||
val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id))
|
||||
val newList = set.sorted().takeLast(200).toSet()
|
||||
PrefManager.setCustomVal(key, newList)
|
||||
binding.statusUserAvatar.loadImage(story.user?.avatar?.large)
|
||||
binding.statusUserName.text = story.user?.name
|
||||
binding.statusUserTime.text = ActivityItemBuilder.getDateTime(story.createdAt)
|
||||
binding.statusUserContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(context, ProfileActivity::class.java).putExtra("userId", story.userId),
|
||||
null
|
||||
)
|
||||
}
|
||||
fun visible(isList: Boolean) {
|
||||
val visible = if (isList) View.VISIBLE else View.GONE
|
||||
val gone = if (isList) View.GONE else View.VISIBLE
|
||||
binding.textActivity.visibility = gone
|
||||
binding.textActivityContainer.visibility = gone
|
||||
binding.infoText.visibility = visible
|
||||
binding.coverImage.visibility = visible
|
||||
binding.infoText.visibility = if (isList) View.VISIBLE else View.INVISIBLE
|
||||
binding.infoText.text = ""
|
||||
binding.contentImageViewKen.visibility = visible
|
||||
binding.contentImageView.visibility = visible
|
||||
}
|
||||
|
||||
when (story.typename) {
|
||||
"ListActivity" -> {
|
||||
visible(true)
|
||||
val text = "${
|
||||
story.status?.replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(Locale.ROOT)
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
} ${story.progress ?: story.media?.title?.userPreferred} " + if (story.status?.contains(
|
||||
"completed"
|
||||
) == false && !story.status.contains("plans") && !story.status.contains(
|
||||
"repeating"
|
||||
)
|
||||
) {
|
||||
"of ${story.media?.title?.userPreferred}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
binding.infoText.text = text
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
blurImage(
|
||||
if (bannerAnimations) binding.contentImageViewKen else binding.contentImageView,
|
||||
story.media?.bannerImage ?: story.media?.coverImage?.extraLarge
|
||||
)
|
||||
binding.coverImage.loadImage(story.media?.coverImage?.extraLarge)
|
||||
binding.coverImage.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(context, MediaDetailsActivity::class.java).putExtra(
|
||||
"mediaId",
|
||||
story.media?.id
|
||||
),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
activity,
|
||||
binding.coverImage,
|
||||
ViewCompat.getTransitionName(binding.coverImage)!!
|
||||
).toBundle()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"TextActivity" -> {
|
||||
visible(false)
|
||||
if (!(context as android.app.Activity).isDestroyed) {
|
||||
val markwon = buildMarkwon(context, false)
|
||||
markwon.setMarkdown(
|
||||
binding.textActivity, AniMarkdown.getBasicAniHTML(story.text ?: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"MessageActivity" -> {
|
||||
visible(false)
|
||||
if (!(context as android.app.Activity).isDestroyed) {
|
||||
val markwon = buildMarkwon(context, false)
|
||||
markwon.setMarkdown(
|
||||
binding.textActivity, AniMarkdown.getBasicAniHTML(story.message ?: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val userList = arrayListOf<User>()
|
||||
story.likes?.forEach { i ->
|
||||
userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage))
|
||||
}
|
||||
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
|
||||
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
|
||||
binding.activityLikeCount.text = story.likeCount.toString()
|
||||
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
|
||||
binding.activityLikeContainer.setOnClickListener {
|
||||
like()
|
||||
}
|
||||
binding.activityLikeContainer.setOnLongClickListener {
|
||||
val context = activity
|
||||
UsersDialogFragment().apply {
|
||||
userList(userList)
|
||||
show(context.supportFragmentManager, "dialog")
|
||||
}
|
||||
true
|
||||
}
|
||||
binding.androidStoriesLoadingView.visibility = View.GONE
|
||||
timer.start()
|
||||
}
|
||||
|
||||
fun like() {
|
||||
val story = activityList[storyIndex - 1]
|
||||
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
|
||||
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
scope.launch {
|
||||
val res = Anilist.query.toggleLike(story.id, "ACTIVITY")
|
||||
withContext(Dispatchers.Main) {
|
||||
if (res != null) {
|
||||
if (story.isLiked == true) {
|
||||
story.likeCount = story.likeCount?.minus(1)
|
||||
} else {
|
||||
story.likeCount = story.likeCount?.plus(1)
|
||||
}
|
||||
binding.activityLikeCount.text = (story.likeCount ?: 0).toString()
|
||||
story.isLiked = !story.isLiked!!
|
||||
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
|
||||
|
||||
} else {
|
||||
snackString("Failed to like activity")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
app/src/main/java/ani/dantotsu/home/status/StoryTimer.kt
Normal file
64
app/src/main/java/ani/dantotsu/home/status/StoryTimer.kt
Normal file
@@ -0,0 +1,64 @@
|
||||
package ani.dantotsu.home.status
|
||||
|
||||
import android.os.CountDownTimer
|
||||
|
||||
class StoryTimer(
|
||||
private val updateInterval: Long
|
||||
) {
|
||||
private lateinit var timer: CountDownTimer
|
||||
private var prevVal = 0
|
||||
private var pauseLength = 0L
|
||||
var onTimerCompleted: () -> Unit = {}
|
||||
var percentTick: (Int) -> Unit = {}
|
||||
var timeLeft: Long = 0
|
||||
private set
|
||||
|
||||
fun start(durationInMillis: Long = updateInterval) {
|
||||
cancel()
|
||||
timer = object : CountDownTimer(durationInMillis, 1) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
timeLeft = millisUntilFinished
|
||||
val percent =
|
||||
((pauseLength + durationInMillis - millisUntilFinished) * 100 / (pauseLength + durationInMillis)).toInt()
|
||||
if (percent != prevVal) {
|
||||
percentTick.invoke(percent)
|
||||
prevVal = percent
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
onTimerCompleted.invoke()
|
||||
pauseLength = 0
|
||||
}
|
||||
}
|
||||
timer.start()
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
if (::timer.isInitialized) {
|
||||
timer.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
if (::timer.isInitialized) {
|
||||
timer.cancel()
|
||||
pauseLength = updateInterval - timeLeft
|
||||
}
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
if (::timer.isInitialized && timeLeft > 0) {
|
||||
start(timeLeft)
|
||||
timer.start()
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnTimerCompletedListener(onTimerCompleted: () -> Unit) {
|
||||
this.onTimerCompleted = onTimerCompleted
|
||||
}
|
||||
|
||||
fun setOnPercentTickListener(percentTick: (Int) -> Unit) {
|
||||
this.percentTick = percentTick
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package ani.dantotsu.home.status
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.ItemUserStatusBinding
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
|
||||
class UserStatusAdapter(private val user: ArrayList<User>) :
|
||||
RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() {
|
||||
|
||||
inner class UsersViewHolder(val binding: ItemUserStatusBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
StatusActivity.user = user
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
Intent(
|
||||
itemView.context,
|
||||
StatusActivity::class.java
|
||||
).putExtra("position", bindingAdapterPosition),
|
||||
null
|
||||
)
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
Intent(
|
||||
itemView.context,
|
||||
ProfileActivity::class.java
|
||||
).putExtra("userId", user[bindingAdapterPosition].id),
|
||||
null
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsersViewHolder {
|
||||
return UsersViewHolder(
|
||||
ItemUserStatusBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: UsersViewHolder, position: Int) {
|
||||
val b = holder.binding
|
||||
setAnimation(b.root.context, b.root)
|
||||
val user = user[position]
|
||||
b.profileUserAvatar.loadImage(user.pfp)
|
||||
b.profileUserName.text = if (Anilist.userid == user.id) getAppString(R.string.your_story) else user.name
|
||||
val watchedActivity = PrefManager.getCustomVal<Set<Int>>("activities", setOf())
|
||||
val booleanList = user.activity.map { watchedActivity.contains(it.id) }
|
||||
b.profileUserStatusIndicator.setParts(user.activity.size, booleanList, user.id == Anilist.userid)
|
||||
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = user.size
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ani.dantotsu.home.status.listener
|
||||
|
||||
interface StoriesCallback {
|
||||
fun onStoriesEnd()
|
||||
fun onStoriesStart()
|
||||
}
|
||||
@@ -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) }
|
||||
|
||||
@@ -15,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 =
|
||||
@@ -23,7 +23,7 @@ class AuthorAdapter(
|
||||
return AuthorViewHolder(binding)
|
||||
}
|
||||
|
||||
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,7 +1,6 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
@@ -14,6 +13,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
@@ -39,15 +39,10 @@ class CalendarActivity : AppCompatActivity() {
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
|
||||
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
val primaryColor = typedValue.data
|
||||
val typedValue3 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||
val primaryTextColor = typedValue3.data
|
||||
val typedValue4 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
|
||||
val secondaryTextColor = typedValue4.data
|
||||
|
||||
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
|
||||
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
|
||||
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
|
||||
|
||||
window.statusBarColor = primaryColor
|
||||
window.navigationBarColor = primaryColor
|
||||
|
||||
@@ -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
|
||||
@@ -28,6 +28,7 @@ class CharacterAdapter(
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val character = characterList[position]
|
||||
val whitespace = "${character.role} "
|
||||
character.voiceActor
|
||||
binding.itemCompactRelation.text = whitespace
|
||||
binding.itemCompactImage.loadImage(character.image)
|
||||
binding.itemCompactTitle.text = character.name
|
||||
|
||||
@@ -95,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(
|
||||
|
||||
@@ -2,7 +2,9 @@ package ani.dantotsu.media
|
||||
|
||||
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
|
||||
@@ -27,16 +29,28 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
||||
"${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
|
||||
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
|
||||
|
||||
@@ -66,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,
|
||||
|
||||
@@ -99,7 +100,7 @@ data class Media(
|
||||
startDate = apiMedia.startDate,
|
||||
endDate = apiMedia.endDate,
|
||||
favourites = apiMedia.favourites,
|
||||
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000},
|
||||
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000 },
|
||||
anime = if (apiMedia.type == MediaType.ANIME) Anime(
|
||||
totalEpisodes = apiMedia.episodes,
|
||||
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
|
||||
@@ -114,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!!) {
|
||||
|
||||
@@ -149,14 +149,22 @@ class MediaAdaptor(
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
)
|
||||
if (media.anime != null) {
|
||||
val itemTotal = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(R.string.episode_singular)
|
||||
val itemTotal = " " + if ((media.anime.totalEpisodes
|
||||
?: 0) != 1
|
||||
) 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) {
|
||||
val itemTotal = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(R.string.chapter_singular)
|
||||
val itemTotal = " " + if ((media.manga.totalChapters
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(
|
||||
R.string.chapter_singular
|
||||
)
|
||||
b.itemTotal.text = itemTotal
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@@ -183,7 +191,10 @@ class MediaAdaptor(
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover)
|
||||
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
|
||||
@@ -232,7 +243,10 @@ class MediaAdaptor(
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover)
|
||||
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
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.TypedValue
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
@@ -38,6 +37,7 @@ import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
@@ -109,7 +109,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
// 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) {
|
||||
@@ -125,9 +127,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
}
|
||||
val navBarRightMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) navBarHeight else 0
|
||||
val navBarBottomMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) 0 else navBarHeight
|
||||
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
rightMargin = navBarRightMargin
|
||||
bottomMargin = navBarBottomMargin
|
||||
@@ -168,7 +172,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
binding.mediaCoverImage.loadImage(media.cover)
|
||||
binding.mediaCoverImage.setOnLongClickListener {
|
||||
val coverTitle = "${media.userPreferredName}[Cover]"
|
||||
val coverTitle = getString(R.string.cover, media.userPreferredName)
|
||||
ImageViewDialog.newInstance(
|
||||
this,
|
||||
coverTitle,
|
||||
@@ -188,7 +192,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
|
||||
override fun onLongClick(event: MotionEvent) {
|
||||
val bannerTitle = "${media.userPreferredName}[Banner]"
|
||||
val bannerTitle = getString(R.string.banner, media.userPreferredName)
|
||||
ImageViewDialog.newInstance(
|
||||
this@MediaDetailsActivity,
|
||||
bannerTitle,
|
||||
@@ -246,22 +250,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
@SuppressLint("ResourceType")
|
||||
fun total() {
|
||||
val text = SpannableStringBuilder().apply {
|
||||
val mediaTypedValue = TypedValue()
|
||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
mediaTypedValue,
|
||||
true
|
||||
)
|
||||
val white = mediaTypedValue.data
|
||||
|
||||
val white = this@MediaDetailsActivity.getThemeColor(com.google.android.material.R.attr.colorOnBackground)
|
||||
if (media.userStatus != null) {
|
||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSecondary,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
bold { color(typedValue.data) { append("${media.userProgress}") } }
|
||||
val colorSecondary = getThemeColor(com.google.android.material.R.attr.colorSecondary)
|
||||
bold { color(colorSecondary) { append("${media.userProgress}") } }
|
||||
append(
|
||||
if (media.anime != null) getString(R.string.episodes_out_of) else getString(
|
||||
R.string.chapters_out_of
|
||||
@@ -345,7 +339,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
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,
|
||||
@@ -368,7 +368,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
} else {
|
||||
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
|
||||
}
|
||||
val commentTab = navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
|
||||
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)
|
||||
@@ -412,10 +413,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val rightMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) navBarHeight else 0
|
||||
val bottomMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight
|
||||
val params : ViewGroup.MarginLayoutParams =
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) 0 else navBarHeight
|
||||
val params: ViewGroup.MarginLayoutParams =
|
||||
navBar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.updateMargins(right = rightMargin, bottom = bottomMargin)
|
||||
}
|
||||
@@ -448,6 +451,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
SupportedMedia.MANGA -> MangaReadFragment()
|
||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val fragment = CommentsFragment()
|
||||
val bundle = Bundle()
|
||||
|
||||
@@ -56,9 +56,11 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
media.anime != null -> {
|
||||
AnimeSources.list.size - 1
|
||||
}
|
||||
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
|
||||
MangaSources.list.size - 1
|
||||
}
|
||||
|
||||
else -> {
|
||||
NovelSources.list.size - 1
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.countDown
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||
import ani.dantotsu.databinding.FragmentMediaInfoBinding
|
||||
@@ -38,8 +37,11 @@ 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.isOnline
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
@@ -79,7 +81,7 @@ class MediaInfoFragment : Fragment() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val model: MediaDetailsViewModel by activityViewModels()
|
||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode) || !isOnline(requireContext())
|
||||
binding.mediaInfoProgressBar.isGone = loaded
|
||||
binding.mediaInfoContainer.isVisible = loaded
|
||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||
@@ -145,7 +147,8 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||
val seasonInfo = "${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
|
||||
val seasonInfo =
|
||||
"${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
|
||||
binding.mediaInfoSeason.text = seasonInfo
|
||||
|
||||
if (media.anime.mainStudio != null) {
|
||||
@@ -182,9 +185,9 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||
val infoTotal = if (media.anime.nextAiringEpisode != null)
|
||||
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
|
||||
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
|
||||
else
|
||||
(media.anime.totalEpisodes ?: "~").toString()
|
||||
(media.anime.totalEpisodes ?: "~").toString()
|
||||
binding.mediaInfoTotal.text = infoTotal
|
||||
|
||||
} else if (media.manga != null) {
|
||||
@@ -213,7 +216,8 @@ class MediaInfoFragment : Fragment() {
|
||||
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
val infoDesc = tripleTab + 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 {
|
||||
@@ -225,8 +229,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 }
|
||||
|
||||
@@ -248,7 +251,39 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.users.isNullOrEmpty() && !offline) {
|
||||
val users: ArrayList<User> = media.users ?: arrayListOf()
|
||||
if (Anilist.token != null && media.userStatus != null) {
|
||||
users.add(0,
|
||||
User(
|
||||
id = Anilist.userid!!,
|
||||
name = getString(R.string.you),
|
||||
pfp = Anilist.avatar,
|
||||
banner = "",
|
||||
status = media.userStatus,
|
||||
score = media.userScore.toFloat(),
|
||||
progress = media.userProgress,
|
||||
totalEpisodes = media.anime?.totalEpisodes ?: media.manga?.totalChapters,
|
||||
nextAiringEpisode = media.anime?.nextAiringEpisode
|
||||
)
|
||||
)
|
||||
}
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
).apply {
|
||||
itemTitle.visibility = View.GONE
|
||||
itemRecycler.adapter =
|
||||
MediaSocialAdapter(users, type, requireActivity())
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (media.trailer != null && !offline) {
|
||||
@Suppress("DEPRECATION")
|
||||
class MyChrome : WebChromeClient() {
|
||||
|
||||
@@ -20,6 +20,7 @@ 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.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWith
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
@@ -187,7 +188,12 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListPrivate.setOnCheckedChangeListener { _, checked ->
|
||||
media?.isListPrivate = checked
|
||||
}
|
||||
|
||||
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
|
||||
var remove: Boolean? = null
|
||||
binding.mediaListShow.isChecked = media?.id in removeList
|
||||
binding.mediaListShow.setOnCheckedChangeListener { _, checked ->
|
||||
remove = checked
|
||||
}
|
||||
media?.userRepeat?.apply {
|
||||
binding.mediaListRewatch.setText(this.toString())
|
||||
}
|
||||
@@ -253,6 +259,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
if (remove == true) {
|
||||
PrefManager.setCustomVal("removeList", removeList.plus(media!!.id))
|
||||
} else if (remove == false) {
|
||||
PrefManager.setCustomVal("removeList", removeList.minus(media!!.id))
|
||||
}
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.list_updated))
|
||||
dismissAllowingStateLoss()
|
||||
@@ -275,6 +286,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
PrefManager.setCustomVal("removeList", removeList.minus(media?.id))
|
||||
}
|
||||
if (id != null) {
|
||||
Refresh.all()
|
||||
|
||||
@@ -18,6 +18,7 @@ import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -170,7 +171,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListPrivate.setOnCheckedChangeListener { _, checked ->
|
||||
media.isListPrivate = checked
|
||||
}
|
||||
|
||||
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
|
||||
var remove: Boolean? = null
|
||||
binding.mediaListShow.isChecked = media.id in removeList
|
||||
binding.mediaListShow.setOnCheckedChangeListener { _, checked ->
|
||||
remove = checked
|
||||
}
|
||||
binding.mediaListSave.setOnClickListener {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -198,6 +204,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
if (remove == true) {
|
||||
PrefManager.setCustomVal("removeList", removeList.plus(media.id))
|
||||
} else if (remove == false) {
|
||||
PrefManager.setCustomVal("removeList", removeList.minus(media.id))
|
||||
}
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.list_updated))
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ActivityMediaListViewBinding
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import java.util.ArrayList
|
||||
|
||||
class MediaListViewActivity: AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMediaListViewBinding
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMediaListViewBinding.inflate(layoutInflater)
|
||||
ThemeManager(this).applyTheme()
|
||||
initActivity(this)
|
||||
if (!PrefManager.getVal<Boolean>(PrefName.ImmersiveMode)) {
|
||||
this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||
binding.root.fitsSystemWindows = true
|
||||
|
||||
} else {
|
||||
binding.root.fitsSystemWindows = false
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
hideSystemBarsExtendView()
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
}
|
||||
}
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
|
||||
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
|
||||
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
|
||||
|
||||
window.statusBarColor = primaryColor
|
||||
window.navigationBarColor = primaryColor
|
||||
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
val mediaList = intent.getSerialized("media") as? ArrayList<Media> ?: ArrayList()
|
||||
val view = PrefManager.getCustomVal("mediaView", 0)
|
||||
var mediaView: View = when (view) {
|
||||
1 -> binding.mediaList
|
||||
0 -> binding.mediaGrid
|
||||
else -> binding.mediaGrid
|
||||
}
|
||||
mediaView.alpha = 1f
|
||||
fun changeView(mode: Int, current: View) {
|
||||
mediaView.alpha = 0.33f
|
||||
mediaView = current
|
||||
current.alpha = 1f
|
||||
PrefManager.setCustomVal("mediaView", mode)
|
||||
binding.mediaRecyclerView.adapter = MediaAdaptor(mode, mediaList, this)
|
||||
binding.mediaRecyclerView.layoutManager = GridLayoutManager(
|
||||
this,
|
||||
if (mode == 1) 1 else (screenWidth / 120f).toInt()
|
||||
)
|
||||
}
|
||||
binding.mediaList.setOnClickListener {
|
||||
changeView(1, binding.mediaList)
|
||||
}
|
||||
binding.mediaGrid.setOnClickListener {
|
||||
changeView(0, binding.mediaGrid)
|
||||
}
|
||||
|
||||
binding.listTitle.text = intent.getStringExtra("title")
|
||||
binding.mediaRecyclerView.adapter = MediaAdaptor(view, mediaList, this)
|
||||
binding.mediaRecyclerView.layoutManager = GridLayoutManager(
|
||||
this,
|
||||
if (view == 1) 1 else (screenWidth / 120f).toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ object MediaNameAdapter {
|
||||
return if (seasonMatcher.find()) {
|
||||
seasonMatcher.group(2)?.toInt()
|
||||
} else {
|
||||
null
|
||||
text.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ object MediaNameAdapter {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
text.toFloatOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ object MediaNameAdapter {
|
||||
if (failedChapterNumberMatcher.find()) {
|
||||
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
text.toFloatOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
82
app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt
Normal file
82
app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt
Normal file
@@ -0,0 +1,82 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemFollowerGridBinding
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.setAnimation
|
||||
|
||||
class MediaSocialAdapter(
|
||||
val user: ArrayList<User>,
|
||||
val type: String,
|
||||
val activity: FragmentActivity
|
||||
) : RecyclerView.Adapter<MediaSocialAdapter.FollowerGridViewHolder>() {
|
||||
|
||||
inner class FollowerGridViewHolder(val binding: ItemFollowerGridBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowerGridViewHolder {
|
||||
return FollowerGridViewHolder(
|
||||
ItemFollowerGridBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override fun onBindViewHolder(holder: FollowerGridViewHolder, 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" -> if (type == "ANIME") getAppString(R.string.watching) else getAppString(R.string.reading)
|
||||
else -> user.status ?: ""
|
||||
}
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
profileCompactUserProgress.text = user.progress.toString()
|
||||
profileCompactScore.text = score.toString()
|
||||
" | ${user.totalEpisodes ?: "~"}".also { profileCompactTotal.text = it }
|
||||
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 {
|
||||
ContextCompat.startActivity(root.context,
|
||||
Intent(root.context, ProfileActivity::class.java)
|
||||
.putExtra("userId", user.id),
|
||||
null)
|
||||
}
|
||||
profileUserAvatarContainer.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
activity,
|
||||
activity.getString(R.string.avatar, user.name),
|
||||
user.pfp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = user.size
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
enum class MediaType {
|
||||
interface Type {
|
||||
fun asText(): String
|
||||
}
|
||||
|
||||
enum class MediaType : Type {
|
||||
ANIME,
|
||||
MANGA,
|
||||
NOVEL;
|
||||
|
||||
fun asText(): String {
|
||||
override fun asText(): String {
|
||||
return when (this) {
|
||||
ANIME -> "Anime"
|
||||
MANGA -> "Manga"
|
||||
@@ -14,12 +18,38 @@ enum class MediaType {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string : String): MediaType {
|
||||
fun fromText(string: String): MediaType? {
|
||||
return when (string) {
|
||||
"Anime" -> ANIME
|
||||
"Manga" -> MANGA
|
||||
"Novel" -> NOVEL
|
||||
else -> { ANIME }
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +78,10 @@ class SearchActivity : AppCompatActivity() {
|
||||
source = intent.getStringExtra("source"),
|
||||
countryOfOrigin = intent.getStringExtra("country"),
|
||||
season = intent.getStringExtra("season"),
|
||||
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
|
||||
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
|
||||
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
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
@@ -60,6 +59,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
}
|
||||
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)
|
||||
@@ -129,36 +129,42 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
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()
|
||||
@@ -171,6 +177,9 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
popupMenu.show()
|
||||
true
|
||||
}
|
||||
if (activity.result.type != "ANIME") {
|
||||
binding.searchByImage.visibility = View.GONE
|
||||
}
|
||||
binding.searchByImage.setOnClickListener {
|
||||
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
|
||||
}
|
||||
@@ -299,14 +308,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +332,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
}
|
||||
|
||||
|
||||
class SearchChipAdapter(val activity: SearchActivity, private val searchAdapter: SearchAdapter) :
|
||||
class SearchChipAdapter(
|
||||
val activity: SearchActivity,
|
||||
private val searchAdapter: SearchAdapter
|
||||
) :
|
||||
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||
private var chips = activity.result.toChipList()
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
setSortByFilterImage()
|
||||
|
||||
binding.resetSearchFilter.setOnClickListener {
|
||||
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
|
||||
val rotateAnimation =
|
||||
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
|
||||
rotateAnimation.duration = 500
|
||||
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
|
||||
rotateAnimation.start()
|
||||
@@ -113,7 +114,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
binding.resetSearchFilter.setOnLongClickListener {
|
||||
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
|
||||
val rotateAnimation =
|
||||
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
|
||||
rotateAnimation.duration = 500
|
||||
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
|
||||
rotateAnimation.start()
|
||||
@@ -125,8 +127,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
activity.result.apply {
|
||||
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
|
||||
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
|
||||
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()
|
||||
@@ -206,21 +210,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
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)
|
||||
@@ -257,7 +265,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
binding.searchFilterCancel.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
val format = if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
|
||||
val format =
|
||||
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
|
||||
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
|
||||
binding.searchStatus.setAdapter(
|
||||
ArrayAdapter(
|
||||
|
||||
@@ -5,6 +5,7 @@ import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -12,7 +13,6 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class SubtitleDownloader {
|
||||
|
||||
@@ -20,28 +20,33 @@ class SubtitleDownloader {
|
||||
//doesn't really download the subtitles -\_(o_o)_/-
|
||||
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>()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
return@withContext try {
|
||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
val response = networkHelper.client.newCall(request).execute()
|
||||
val response = networkHelper.client.newCall(request).execute()
|
||||
|
||||
// Check if response is successful
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body.string()
|
||||
// Check if response is successful
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body.string()
|
||||
|
||||
|
||||
val subtitleType = when {
|
||||
responseBody.contains("[Script Info]") -> SubtitleType.ASS
|
||||
responseBody.contains("WEBVTT") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
val subtitleType = when {
|
||||
responseBody.contains("[Script Info]") -> SubtitleType.ASS
|
||||
responseBody.contains("WEBVTT") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
}
|
||||
|
||||
subtitleType
|
||||
} else {
|
||||
SubtitleType.UNKNOWN
|
||||
}
|
||||
|
||||
return@withContext subtitleType
|
||||
} else {
|
||||
return@withContext SubtitleType.UNKNOWN
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
SubtitleType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +61,8 @@ class SubtitleDownloader {
|
||||
context,
|
||||
downloadedType.type,
|
||||
false,
|
||||
downloadedType.title,
|
||||
downloadedType.chapter
|
||||
downloadedType.titleName,
|
||||
downloadedType.chapterName
|
||||
) ?: throw Exception("Could not create directory")
|
||||
val type = loadSubtitleType(url)
|
||||
directory.findFile("subtitle.${type}")?.delete()
|
||||
|
||||
@@ -17,11 +17,11 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.countDown
|
||||
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
|
||||
@@ -330,6 +330,7 @@ class AnimeWatchAdapter(
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
chip.text = chipText
|
||||
chip.setTextColor(
|
||||
@@ -412,10 +413,12 @@ class AnimeWatchAdapter(
|
||||
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeSourceContinueText.text =
|
||||
currActivity()!!.getString(R.string.continue_episode, ep.number, if (ep.filler)
|
||||
currActivity()!!.getString(R.string.filler_tag)
|
||||
else
|
||||
"", 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)
|
||||
}
|
||||
@@ -441,11 +444,14 @@ class AnimeWatchAdapter(
|
||||
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)
|
||||
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 } }
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||
setLanguageList(0, nextIndex)
|
||||
@@ -483,7 +489,7 @@ class AnimeWatchAdapter(
|
||||
val adapter = ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||
parser.extension.sources.map { LanguageMapper.getLanguageName(it.lang) }
|
||||
)
|
||||
val items = adapter.count
|
||||
|
||||
@@ -500,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils
|
||||
@@ -25,7 +24,6 @@ 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
|
||||
@@ -35,14 +33,15 @@ 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.findValidName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
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.MediaType
|
||||
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
|
||||
@@ -200,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
|
||||
@@ -366,7 +371,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names =
|
||||
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||
allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray()
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, -1) { dialog, which ->
|
||||
@@ -425,17 +430,27 @@ class AnimeWatchFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||
activity?.let{
|
||||
activity?.let {
|
||||
if (!hasDirAccess(it)) {
|
||||
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
|
||||
if (success) {
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||
model.onEpisodeClick(
|
||||
media,
|
||||
i,
|
||||
requireActivity().supportFragmentManager,
|
||||
isDownload = true
|
||||
)
|
||||
} else {
|
||||
snackString("Permission is required to download")
|
||||
snackString(getString(R.string.download_permission_required))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||
model.onEpisodeClick(
|
||||
media,
|
||||
i,
|
||||
requireActivity().supportFragmentManager,
|
||||
isDownload = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,10 +487,6 @@ class AnimeWatchFragment : Fragment() {
|
||||
)
|
||||
) {
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
||||
taskName,
|
||||
""
|
||||
) ?: ""
|
||||
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
||||
episodeAdapter.deleteDownload(i)
|
||||
}
|
||||
@@ -542,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().findValidName()) {
|
||||
episodeAdapter.stopDownload(download.chapter)
|
||||
if (media.compareName(download.titleName)) {
|
||||
episodeAdapter.stopDownload(download.chapterName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getDirSize
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
|
||||
@@ -48,6 +48,8 @@ import android.widget.ImageButton
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -60,12 +62,16 @@ import androidx.media3.cast.CastPlayer
|
||||
import androidx.media3.cast.SessionAvailabilityListener
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
|
||||
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackGroup
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
@@ -76,7 +82,9 @@ import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.exoplayer.util.EventLogger
|
||||
import androidx.media3.session.MediaSession
|
||||
@@ -105,6 +113,7 @@ import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.getCurrentBrightnessValue
|
||||
import ani.dantotsu.hideSystemBars
|
||||
@@ -119,6 +128,7 @@ import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.others.AniSkip
|
||||
import ani.dantotsu.others.AniSkip.getType
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.others.ResettableTimer
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
@@ -128,16 +138,17 @@ import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.PlayerSettingsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.toPx
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
@@ -153,10 +164,12 @@ import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
@@ -180,6 +193,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
private lateinit var cacheFactory: CacheDataSource.Factory
|
||||
private lateinit var playbackParameters: PlaybackParameters
|
||||
private lateinit var mediaItem: MediaItem
|
||||
private lateinit var mediaSource: MergingMediaSource
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
private lateinit var binding: ActivityExoplayerBinding
|
||||
@@ -189,6 +203,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
private lateinit var exoSettings: ImageButton
|
||||
private lateinit var exoSubtitle: ImageButton
|
||||
private lateinit var exoSubtitleView: SubtitleView
|
||||
private lateinit var exoAudioTrack: ImageButton
|
||||
private lateinit var exoRotate: ImageButton
|
||||
private lateinit var exoSpeed: ImageButton
|
||||
private lateinit var exoScreen: ImageButton
|
||||
@@ -211,10 +226,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
private var orientationListener: OrientationEventListener? = null
|
||||
|
||||
private var downloadId: String? = null
|
||||
private var hasExtSubtitles = false
|
||||
private var audioLanguages = mutableListOf<String>()
|
||||
|
||||
companion object {
|
||||
var initialized = false
|
||||
lateinit var media: Media
|
||||
|
||||
private const val DEFAULT_MIN_BUFFER_MS = 600000
|
||||
private const val DEFAULT_MAX_BUFFER_MS = 600000
|
||||
private const val BUFFER_FOR_PLAYBACK_MS = 2500
|
||||
private const val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000
|
||||
}
|
||||
|
||||
private lateinit var episode: Episode
|
||||
@@ -282,7 +304,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
}
|
||||
playerView.findViewById<View>(androidx.media3.ui.R.id.exo_buffering).translationY =
|
||||
(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8f.px)).dp
|
||||
(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8.toPx)).dp
|
||||
exoBrightnessCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
marginEnd =
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
|
||||
@@ -372,16 +394,31 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
6 -> ResourcesCompat.getFont(this, R.font.blocky)
|
||||
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
|
||||
}
|
||||
playerView.subtitleView?.setStyle(
|
||||
CaptionStyleCompat(
|
||||
primaryColor,
|
||||
subBackground,
|
||||
subWindow,
|
||||
outline,
|
||||
secondaryColor,
|
||||
font
|
||||
val fontSize = PrefManager.getVal<Int>(PrefName.FontSize).toFloat()
|
||||
|
||||
playerView.subtitleView?.let { subtitles ->
|
||||
subtitles.setApplyEmbeddedStyles(false)
|
||||
subtitles.setApplyEmbeddedFontSizes(false)
|
||||
|
||||
subtitles.setStyle(
|
||||
CaptionStyleCompat(
|
||||
primaryColor,
|
||||
subBackground,
|
||||
subWindow,
|
||||
outline,
|
||||
secondaryColor,
|
||||
font
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
subtitles.alpha =
|
||||
when (PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
|
||||
true -> PrefManager.getVal(PrefName.SubAlpha)
|
||||
false -> 0f
|
||||
}
|
||||
|
||||
subtitles.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -414,6 +451,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
exoSource = playerView.findViewById(R.id.exo_source)
|
||||
exoSettings = playerView.findViewById(R.id.exo_settings)
|
||||
exoSubtitle = playerView.findViewById(R.id.exo_sub)
|
||||
exoAudioTrack = playerView.findViewById(R.id.exo_audio)
|
||||
exoSubtitleView = playerView.findViewById(androidx.media3.ui.R.id.exo_subtitles)
|
||||
exoRotate = playerView.findViewById(R.id.exo_rotate)
|
||||
exoSpeed = playerView.findViewById(androidx.media3.ui.R.id.exo_playback_speed)
|
||||
@@ -481,17 +519,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
setupSubFormatting(playerView)
|
||||
|
||||
|
||||
playerView.subtitleView?.alpha = when (PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
|
||||
true -> PrefManager.getVal(PrefName.SubAlpha)
|
||||
false -> 0f
|
||||
}
|
||||
val fontSize = PrefManager.getVal<Int>(PrefName.FontSize).toFloat()
|
||||
playerView.subtitleView?.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
currentWindow = savedInstanceState.getInt(resumeWindow)
|
||||
playbackPosition = savedInstanceState.getLong(resumePosition)
|
||||
@@ -903,7 +932,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
isFastForwarding = true
|
||||
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
|
||||
fastForward.visibility = View.VISIBLE
|
||||
fastForward.text = "${exoPlayer.playbackParameters.speed}x"
|
||||
val speedText = "${exoPlayer.playbackParameters.speed}x"
|
||||
fastForward.text = speedText
|
||||
}
|
||||
|
||||
fun stopFastForward() {
|
||||
@@ -1196,7 +1226,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
putExtra("subtitle", subtitle)
|
||||
}
|
||||
exoPlayer.pause()
|
||||
startActivity(intent)
|
||||
onChangeSettings.launch(intent)
|
||||
}
|
||||
|
||||
//Speed
|
||||
@@ -1352,7 +1382,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
val ext = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return
|
||||
extractor = ext
|
||||
video = ext.videos.getOrNull(episode.selectedVideo) ?: return
|
||||
|
||||
subtitle = intent.getSerialized("subtitle")
|
||||
?: when (val subLang: String? =
|
||||
PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) {
|
||||
@@ -1369,18 +1398,23 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
|
||||
//Subtitles
|
||||
exoSubtitle.isVisible = ext.subtitles.isNotEmpty()
|
||||
exoSubtitle.setOnClickListener {
|
||||
subClick()
|
||||
hasExtSubtitles = ext.subtitles.isNotEmpty()
|
||||
if (hasExtSubtitles) {
|
||||
exoSubtitle.isVisible = hasExtSubtitles
|
||||
exoSubtitle.setOnClickListener {
|
||||
subClick()
|
||||
}
|
||||
}
|
||||
var sub: MediaItem.SubtitleConfiguration? = null
|
||||
if (subtitle != null) {
|
||||
val sub: MutableList<MediaItem.SubtitleConfiguration> =
|
||||
emptyList<MediaItem.SubtitleConfiguration>().toMutableList()
|
||||
ext.subtitles.forEach { subtitle ->
|
||||
val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle.file.url
|
||||
//var localFile: String? = null
|
||||
if (subtitle?.type == SubtitleType.UNKNOWN) {
|
||||
if (subtitle.type == SubtitleType.UNKNOWN) {
|
||||
runBlocking {
|
||||
val type = SubtitleDownloader.loadSubtitleType(subtitle!!.file.url)
|
||||
val fileUri = Uri.parse(subtitle!!.file.url)
|
||||
sub = MediaItem.SubtitleConfiguration
|
||||
val type = SubtitleDownloader.loadSubtitleType(subtitleUrl)
|
||||
val fileUri = Uri.parse(subtitleUrl)
|
||||
sub += MediaItem.SubtitleConfiguration
|
||||
.Builder(fileUri)
|
||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
||||
.setMimeType(
|
||||
@@ -1392,16 +1426,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
)
|
||||
.setId("69")
|
||||
.setLanguage(subtitle.language)
|
||||
.build()
|
||||
}
|
||||
println("sub: $sub")
|
||||
} else {
|
||||
val subUri = Uri.parse(subtitle!!.file.url)
|
||||
sub = MediaItem.SubtitleConfiguration
|
||||
val subUri = Uri.parse(subtitleUrl)
|
||||
sub += MediaItem.SubtitleConfiguration
|
||||
.Builder(subUri)
|
||||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
||||
.setMimeType(
|
||||
when (subtitle?.type) {
|
||||
when (subtitle.type) {
|
||||
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
||||
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
||||
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
||||
@@ -1409,6 +1444,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
)
|
||||
.setId("69")
|
||||
.setLanguage(subtitle.language)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1450,52 +1486,90 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
|
||||
val downloadedMediaItem = if (ext.server.offline) {
|
||||
val key = ext.server.name
|
||||
val titleName = ext.server.name.split("/").first()
|
||||
val episodeName = ext.server.name.split("/").last()
|
||||
downloadId = PrefManager.getAnimeDownloadPreferences()
|
||||
.getString("$titleName - $episodeName", null)
|
||||
?: PrefManager.getAnimeDownloadPreferences()
|
||||
.getString(ext.server.name, null)
|
||||
val exoItem = if (downloadId != null) {
|
||||
Helper.downloadManager(this)
|
||||
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
|
||||
} else null
|
||||
if (exoItem != null) {
|
||||
exoItem
|
||||
} else {
|
||||
|
||||
val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
|
||||
if (directory != null) {
|
||||
val files = directory.listFiles()
|
||||
println(files)
|
||||
val docFile = directory.listFiles().firstOrNull {
|
||||
it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true
|
||||
}
|
||||
if (docFile != null) {
|
||||
val uri = docFile.uri
|
||||
MediaItem.Builder().setUri(uri).setMimeType(mimeType).build()
|
||||
val directory =
|
||||
getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
|
||||
if (directory != null) {
|
||||
val files = directory.listFiles()
|
||||
println(files)
|
||||
val docFile = directory.listFiles().firstOrNull {
|
||||
it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true
|
||||
}
|
||||
if (docFile != null) {
|
||||
val uri = docFile.uri
|
||||
val downloadedMimeType = when (docFile.extension) {
|
||||
"mp4" -> MimeTypes.APPLICATION_MP4
|
||||
"mkv" -> MimeTypes.APPLICATION_MATROSKA
|
||||
else -> MimeTypes.APPLICATION_MP4
|
||||
}
|
||||
MediaItem.Builder().setUri(uri).setMimeType(downloadedMimeType).build()
|
||||
} else {
|
||||
snackString("File not found")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
snackString("File not found")
|
||||
snackString("Directory not found")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
snackString("Directory not found")
|
||||
null
|
||||
}
|
||||
|
||||
} else null
|
||||
|
||||
mediaItem = if (downloadedMediaItem == null) {
|
||||
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
|
||||
Logger.log("url: ${video!!.file.url}")
|
||||
Logger.log("mimeType: $mimeType")
|
||||
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = listOfNotNull(sub)
|
||||
builder.setSubtitleConfigurations(listofnotnullsubs)
|
||||
}
|
||||
builder.setSubtitleConfigurations(sub)
|
||||
builder.build()
|
||||
} else {
|
||||
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = listOfNotNull(sub)
|
||||
val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build()
|
||||
if (sub.isNotEmpty()) {
|
||||
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
|
||||
val addLanguage = sub[0].buildUpon().setLanguage("en").build()
|
||||
addedSubsDownloadedMediaItem.setSubtitleConfigurations(listOf(addLanguage))
|
||||
episode.selectedSubtitle = 0
|
||||
addedSubsDownloadedMediaItem.build()
|
||||
} else {
|
||||
downloadedMediaItem
|
||||
}
|
||||
addedSubsDownloadedMediaItem.build()
|
||||
}
|
||||
|
||||
val audioMediaItem = mutableListOf<MediaItem>()
|
||||
audioLanguages.clear()
|
||||
ext.audioTracks.forEach {
|
||||
val code = LanguageMapper.getLanguageCode(it.lang)
|
||||
audioLanguages.add(code)
|
||||
audioMediaItem.add(
|
||||
MediaItem.Builder()
|
||||
.setUri(it.url)
|
||||
.setMimeType(MimeTypes.AUDIO_UNKNOWN)
|
||||
.setTag(code)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
val audioSources = audioMediaItem.map { mediaItem ->
|
||||
if (mediaItem.localConfiguration?.uri.toString().endsWith(".m3u8")) {
|
||||
HlsMediaSource.Factory(cacheFactory).createMediaSource(mediaItem)
|
||||
} else {
|
||||
DefaultMediaSourceFactory(cacheFactory).createMediaSource(mediaItem)
|
||||
}
|
||||
}.toTypedArray()
|
||||
val videoMediaSource = DefaultMediaSourceFactory(cacheFactory)
|
||||
.createMediaSource(mediaItem)
|
||||
mediaSource = MergingMediaSource(videoMediaSource, *audioSources)
|
||||
|
||||
|
||||
//Source
|
||||
exoSource.setOnClickListener {
|
||||
@@ -1504,22 +1578,22 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
|
||||
//Quality Track
|
||||
trackSelector = DefaultTrackSelector(this)
|
||||
trackSelector.setParameters(
|
||||
trackSelector.buildUponParameters()
|
||||
.setAllowVideoMixedMimeTypeAdaptiveness(true)
|
||||
.setAllowVideoNonSeamlessAdaptiveness(true)
|
||||
.setSelectUndeterminedTextLanguage(true)
|
||||
.setAllowAudioMixedMimeTypeAdaptiveness(true)
|
||||
.setAllowMultipleAdaptiveSelections(true)
|
||||
.setPreferredTextLanguage(subtitle?.language ?: "en")
|
||||
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.setRendererDisabled(TRACK_TYPE_VIDEO, false)
|
||||
.setRendererDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||
.setRendererDisabled(C.TRACK_TYPE_TEXT, false)
|
||||
.setMaxVideoSize(1, 1)
|
||||
//.setOverrideForType(
|
||||
// TrackSelectionOverride(trackSelector, 2))
|
||||
)
|
||||
val parameters = trackSelector.buildUponParameters()
|
||||
.setAllowVideoMixedMimeTypeAdaptiveness(true)
|
||||
.setAllowVideoNonSeamlessAdaptiveness(true)
|
||||
.setSelectUndeterminedTextLanguage(true)
|
||||
.setAllowAudioMixedMimeTypeAdaptiveness(true)
|
||||
.setAllowMultipleAdaptiveSelections(true)
|
||||
.setPreferredTextLanguage(subtitle?.language ?: Locale.getDefault().language)
|
||||
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.setRendererDisabled(TRACK_TYPE_VIDEO, false)
|
||||
.setRendererDisabled(TRACK_TYPE_AUDIO, false)
|
||||
.setRendererDisabled(TRACK_TYPE_TEXT, false)
|
||||
.setMaxVideoSize(1, 1)
|
||||
// .setOverrideForType(TrackSelectionOverride(trackSelector, TRACK_TYPE_VIDEO))
|
||||
if (PrefManager.getVal(PrefName.SettingsPreferDub))
|
||||
parameters.setPreferredAudioLanguage(Locale.getDefault().language)
|
||||
trackSelector.setParameters(parameters)
|
||||
|
||||
if (playbackPosition != 0L && !changingServer && !PrefManager.getVal<Boolean>(PrefName.AlwaysContinue)) {
|
||||
val time = String.format(
|
||||
@@ -1554,11 +1628,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
|
||||
private fun buildExoplayer() {
|
||||
//Player
|
||||
val DEFAULT_MIN_BUFFER_MS = 600000
|
||||
val DEFAULT_MAX_BUFFER_MS = 600000
|
||||
val BUFFER_FOR_PLAYBACK_MS = 2500
|
||||
val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000
|
||||
|
||||
val loadControl = DefaultLoadControl.Builder()
|
||||
.setBackBuffer(1000 * 60 * 2, true)
|
||||
.setBufferDurationsMs(
|
||||
@@ -1577,7 +1646,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
.build().apply {
|
||||
playWhenReady = true
|
||||
this.playbackParameters = this@ExoplayerView.playbackParameters
|
||||
setMediaItem(mediaItem)
|
||||
setMediaSource(mediaSource)
|
||||
prepare()
|
||||
PrefManager.getCustomVal(
|
||||
"${media.id}_${media.anime!!.selectedEpisode}_max",
|
||||
@@ -1589,7 +1658,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
playerView.player = exoPlayer
|
||||
|
||||
|
||||
try {
|
||||
val rightNow = Calendar.getInstance()
|
||||
mediaSession = MediaSession.Builder(this, exoPlayer)
|
||||
@@ -1602,32 +1670,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
exoPlayer.addListener(this)
|
||||
exoPlayer.addAnalyticsListener(EventLogger())
|
||||
isInitialized = true
|
||||
}
|
||||
/*private fun selectSubtitleTrack() { saving this for later
|
||||
// Get the current track groups
|
||||
val trackGroups = exoPlayer.currentTrackGroups
|
||||
|
||||
// Prepare a track selector parameters builder
|
||||
val parametersBuilder = DefaultTrackSelector.ParametersBuilder(this)
|
||||
|
||||
// Iterate through the track groups to find the subtitle tracks
|
||||
for (i in 0 until trackGroups.length) {
|
||||
val trackGroup = trackGroups[i]
|
||||
for (j in 0 until trackGroup.length) {
|
||||
val trackMetadata = trackGroup.getFormat(j)
|
||||
|
||||
// Check if the track is a subtitle track
|
||||
if (MimeTypes.isText(trackMetadata.sampleMimeType)) {
|
||||
parametersBuilder.setRendererDisabled(i, false) // Enable the renderer for this track group
|
||||
parametersBuilder.setSelectionOverride(i, trackGroups, DefaultTrackSelector.SelectionOverride(j, 0)) // Override to select this track
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasExtSubtitles && !PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
|
||||
onSetTrackGroupOverride(dummyTrack, TRACK_TYPE_TEXT)
|
||||
}
|
||||
|
||||
// Apply the track selector parameters to select the subtitle
|
||||
trackSelector.setParameters(parametersBuilder)
|
||||
}*/
|
||||
val isDisabled = (subtitle == null && hasExtSubtitles)
|
||||
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(TRACK_TYPE_TEXT, isDisabled)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
isPlayerPlaying = exoPlayer.playWhenReady
|
||||
@@ -1802,7 +1855,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
if (isInitialized) {
|
||||
val playerCurrentTime = exoPlayer.currentPosition / 1000
|
||||
currentTimeStamp = model.timeStamps.value?.find { timestamp ->
|
||||
timestamp.interval.startTime < playerCurrentTime && playerCurrentTime < (timestamp.interval.endTime - 1)
|
||||
timestamp.interval.startTime < playerCurrentTime
|
||||
&& playerCurrentTime < (timestamp.interval.endTime - 1)
|
||||
}
|
||||
|
||||
val new = currentTimeStamp
|
||||
@@ -1825,7 +1879,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
if (new == null) {
|
||||
skipTimeButton.visibility = View.GONE
|
||||
exoSkip.visibility = View.VISIBLE
|
||||
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
|
||||
disappeared = false
|
||||
functionstarted = false
|
||||
cancelTimer()
|
||||
@@ -1834,7 +1888,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
|
||||
override fun onFinish() {
|
||||
skipTimeButton.visibility = View.GONE
|
||||
exoSkip.visibility = View.VISIBLE
|
||||
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
|
||||
disappeared = true
|
||||
functionstarted = false
|
||||
cancelTimer()
|
||||
@@ -1855,6 +1909,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (PrefManager.getVal(PrefName.AutoSkipOPED) && (new.skipType == "op" || new.skipType == "ed")
|
||||
&& !skippedTimeStamps.contains(new)
|
||||
@@ -1862,6 +1917,13 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
||||
skippedTimeStamps.add(new)
|
||||
}
|
||||
if (PrefManager.getVal(PrefName.AutoSkipRecap) && new.skipType == "recap" && !skippedTimeStamps.contains(
|
||||
new
|
||||
)
|
||||
) {
|
||||
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
||||
skippedTimeStamps.add(new)
|
||||
}
|
||||
new.skipType.getType()
|
||||
} else {
|
||||
disappeared = false
|
||||
@@ -1876,43 +1938,95 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}, 500)
|
||||
}
|
||||
|
||||
fun onSetTrackGroupOverride(trackGroup: Tracks.Group, type: @C.TrackType Int, index: Int = 0) {
|
||||
val isDisabled = trackGroup.getTrackFormat(0).language == "none"
|
||||
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(TRACK_TYPE_TEXT, isDisabled)
|
||||
.setOverrideForType(
|
||||
TrackSelectionOverride(trackGroup.mediaTrackGroup, index)
|
||||
)
|
||||
.build()
|
||||
if (type == TRACK_TYPE_TEXT) setupSubFormatting(playerView)
|
||||
playerView.subtitleView?.alpha = when (isDisabled) {
|
||||
false -> PrefManager.getVal(PrefName.SubAlpha)
|
||||
true -> 0f
|
||||
}
|
||||
}
|
||||
|
||||
private val dummyTrack = Tracks.Group(
|
||||
TrackGroup("Dummy Track", Format.Builder().apply { setLanguage("none") }.build()),
|
||||
true,
|
||||
intArrayOf(1),
|
||||
booleanArrayOf(false)
|
||||
)
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
val audioTracks: ArrayList<Tracks.Group> = arrayListOf()
|
||||
val subTracks: ArrayList<Tracks.Group> = arrayListOf(dummyTrack)
|
||||
tracks.groups.forEach {
|
||||
println("Track__: $it")
|
||||
println("Track__: ${it.length}")
|
||||
println("Track__: ${it.isSelected}")
|
||||
println("Track__: ${it.type}")
|
||||
println("Track__: ${it.mediaTrackGroup.id}")
|
||||
if (it.type == 3 && it.mediaTrackGroup.id == "1:") {
|
||||
playerView.player?.trackSelectionParameters =
|
||||
playerView.player?.trackSelectionParameters?.buildUpon()
|
||||
?.setOverrideForType(
|
||||
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)
|
||||
)
|
||||
?.build()!!
|
||||
} else if (it.type == 3) {
|
||||
playerView.player?.trackSelectionParameters =
|
||||
playerView.player?.trackSelectionParameters?.buildUpon()
|
||||
?.addOverride(
|
||||
TrackSelectionOverride(it.mediaTrackGroup, listOf())
|
||||
)
|
||||
?.build()!!
|
||||
println(
|
||||
"Track__: $it\nTrack__: ${it.length}\nTrack__: ${it.isSelected}\n" +
|
||||
"Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}"
|
||||
)
|
||||
when (it.type) {
|
||||
TRACK_TYPE_AUDIO -> {
|
||||
if (it.isSupported(true)) audioTracks.add(it)
|
||||
}
|
||||
|
||||
TRACK_TYPE_TEXT -> {
|
||||
if (!hasExtSubtitles) {
|
||||
if (it.isSupported(true)) subTracks.add(it)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println("Track: ${tracks.groups.size}")
|
||||
exoAudioTrack.isVisible = audioTracks.size > 1
|
||||
exoAudioTrack.setOnClickListener {
|
||||
TrackGroupDialogFragment(this, audioTracks, TRACK_TYPE_AUDIO, audioLanguages)
|
||||
.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
if (!hasExtSubtitles) {
|
||||
exoSubtitle.isVisible = subTracks.size > 1
|
||||
exoSubtitle.setOnClickListener {
|
||||
TrackGroupDialogFragment(this, subTracks, TRACK_TYPE_TEXT)
|
||||
.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val onChangeSettings = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { _: ActivityResult ->
|
||||
if (!hasExtSubtitles) {
|
||||
exoPlayer.currentTracks.groups.forEach { trackGroup ->
|
||||
when (trackGroup.type) {
|
||||
TRACK_TYPE_TEXT -> {
|
||||
if (PrefManager.getVal(PrefName.Subtitles)) {
|
||||
onSetTrackGroupOverride(trackGroup, TRACK_TYPE_TEXT)
|
||||
} else {
|
||||
onSetTrackGroupOverride(dummyTrack, TRACK_TYPE_TEXT)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isInitialized) exoPlayer.play()
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
when (error.errorCode) {
|
||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
|
||||
-> {
|
||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> {
|
||||
toast("Source Exception : ${error.message}")
|
||||
isPlayerPlaying = true
|
||||
sourceClick()
|
||||
}
|
||||
|
||||
else
|
||||
-> {
|
||||
else -> {
|
||||
toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
|
||||
Injekt.get<CrashlyticsInterface>().logException(error)
|
||||
}
|
||||
@@ -1922,7 +2036,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
private var isBuffering = true
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
if (playbackState == ExoPlayer.STATE_READY) {
|
||||
|
||||
exoPlayer.play()
|
||||
if (episodeLength == 0f) {
|
||||
episodeLength = exoPlayer.duration.toFloat()
|
||||
@@ -1977,6 +2090,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeIntentLaunch")
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
finishAndRemoveTask()
|
||||
@@ -2006,10 +2120,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
// Cast
|
||||
private fun cast() {
|
||||
val videoURL = video?.file?.url ?: return
|
||||
val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle!!.file.url
|
||||
val shareVideo = Intent(Intent.ACTION_VIEW)
|
||||
shareVideo.setDataAndType(Uri.parse(videoURL), "video/*")
|
||||
shareVideo.setPackage("com.instantbits.cast.webvideo")
|
||||
if (subtitle != null) shareVideo.putExtra("subtitle", subtitle!!.file.url)
|
||||
if (subtitle != null) shareVideo.putExtra("subtitle", subtitleUrl)
|
||||
shareVideo.putExtra(
|
||||
"title",
|
||||
media.userPreferredName + " : Ep " + episodeTitleArr[currentEpisodeIndex]
|
||||
@@ -2205,4 +2320,4 @@ class CustomCastButton : MediaRouteButton {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -24,6 +23,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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
|
||||
@@ -32,6 +33,7 @@ import ani.dantotsu.databinding.ItemStreamBinding
|
||||
import ani.dantotsu.databinding.ItemUrlBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.hideSystemBars
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
@@ -47,12 +49,16 @@ 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
|
||||
@@ -89,10 +95,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
||||
val window = dialog?.window
|
||||
window?.statusBarColor = Color.TRANSPARENT
|
||||
val typedValue = TypedValue()
|
||||
val theme = requireContext().theme
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
window?.navigationBarColor = typedValue.data
|
||||
window?.navigationBarColor = requireContext().getThemeColor(com.google.android.material.R.attr.colorSurface)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -230,11 +233,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
private val externalPlayerResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result: ActivityResult ->
|
||||
Logger.log(result.data.toString())
|
||||
}
|
||||
|
||||
private fun exportMagnetIntent(episode: Episode, video: Video) : Intent {
|
||||
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")
|
||||
@@ -252,32 +256,77 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun startExoplayer(media: Media) {
|
||||
prevEpisode = null
|
||||
|
||||
dismiss()
|
||||
|
||||
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:")) {
|
||||
try {
|
||||
externalPlayerResult.launch(exportMagnetIntent(ep, video))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val amnis = "com.amnis"
|
||||
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
||||
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||
if (torrentExtension.isAvailable()) {
|
||||
val activity = currActivity() ?: requireActivity()
|
||||
launchIO {
|
||||
try {
|
||||
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()
|
||||
} catch (e: Exception) {
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
Logger.log(e)
|
||||
toast("Error starting video")
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
startActivity(Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=$amnis"))
|
||||
)
|
||||
externalPlayerResult.launch(exportMagnetIntent(ep, video))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
startActivity(Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
|
||||
))
|
||||
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
|
||||
@@ -285,7 +334,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
||||
dismiss()
|
||||
if (launch!!) {
|
||||
stopAddingToList()
|
||||
val intent = Intent(activity, ExoplayerView::class.java)
|
||||
ExoplayerView.media = media
|
||||
@@ -343,7 +393,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
fun performClick(position: Int) {
|
||||
try { //bandaid fix for crash
|
||||
try {
|
||||
val extractor = links[position]
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
|
||||
extractor.server.name
|
||||
@@ -403,7 +453,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
SubtitleDownloader.downloadSubtitle(
|
||||
requireContext(),
|
||||
subtitleToDownload!!.file.url,
|
||||
DownloadedType(media!!.mainName(), media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number, MediaType.ANIME)
|
||||
DownloadedType(
|
||||
media!!.mainName(),
|
||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number,
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -430,13 +484,45 @@ 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")
|
||||
@@ -510,9 +596,13 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
if (video.format == VideoType.CONTAINER) {
|
||||
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)
|
||||
}")
|
||||
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
|
||||
|
||||
@@ -67,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)
|
||||
}
|
||||
@@ -107,7 +111,11 @@ 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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
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(
|
||||
private var instance: ExoplayerView,
|
||||
private var trackGroups: ArrayList<Tracks.Group>,
|
||||
private var type: @TrackType Int,
|
||||
private var overrideTrackNames: List<String>? = null
|
||||
) : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSubtitlesBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
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 = overrideTrackNames?.getOrNull(position)
|
||||
?: 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()
|
||||
}
|
||||
}
|
||||
@@ -60,204 +60,211 @@ class CommentItem(
|
||||
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
|
||||
binding = viewBinding
|
||||
setAnimation(binding.root.context, binding.root)
|
||||
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.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
||||
if (comment.tag == null) {
|
||||
viewBinding.commentUserTagLayout.visibility = View.GONE
|
||||
} else {
|
||||
viewBinding.commentUserTagLayout.visibility = View.VISIBLE
|
||||
viewBinding.commentUserTag.text = comment.tag.toString()
|
||||
}
|
||||
replying(isReplying) //sets default text
|
||||
editing(isEditing)
|
||||
if ((comment.replyCount ?: 0) > 0) {
|
||||
viewBinding.commentTotalReplies.visibility = View.VISIBLE
|
||||
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
|
||||
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
|
||||
}
|
||||
viewBinding.commentReply.visibility = View.VISIBLE
|
||||
viewBinding.commentTotalReplies.setOnClickListener {
|
||||
if (repliesVisible) {
|
||||
repliesSection.clear()
|
||||
removeSubCommentIds()
|
||||
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
|
||||
val item = this
|
||||
viewBinding.apply {
|
||||
commentRepliesList.layoutManager =
|
||||
LinearLayoutManager(commentsFragment.activity)
|
||||
commentRepliesList.adapter = adapter
|
||||
val isUserComment = CommentsAPI.userId == comment.userId
|
||||
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
|
||||
markwon.setMarkdown(commentText, comment.content)
|
||||
commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
||||
if (comment.tag == null) {
|
||||
commentUserTagLayout.visibility = View.GONE
|
||||
} else {
|
||||
viewBinding.commentTotalReplies.setText(R.string.hide_replies)
|
||||
repliesSection.clear()
|
||||
commentsFragment.viewReplyCallback(this)
|
||||
repliesVisible = true
|
||||
commentUserTagLayout.visibility = View.VISIBLE
|
||||
commentUserTag.text = comment.tag.toString()
|
||||
}
|
||||
replying(isReplying) //sets default text
|
||||
editing(isEditing)
|
||||
if ((comment.replyCount ?: 0) > 0) {
|
||||
commentTotalReplies.visibility = View.VISIBLE
|
||||
commentRepliesDivider.visibility = View.VISIBLE
|
||||
commentTotalReplies.context.run {
|
||||
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 {
|
||||
commentTotalReplies.visibility = View.GONE
|
||||
commentRepliesDivider.visibility = View.GONE
|
||||
}
|
||||
commentReply.visibility = View.VISIBLE
|
||||
commentTotalReplies.setOnClickListener {
|
||||
if (repliesVisible) {
|
||||
repliesSection.clear()
|
||||
removeSubCommentIds()
|
||||
commentTotalReplies.context.run {
|
||||
commentTotalReplies.text = if (comment.replyCount == 1)
|
||||
getString(R.string.view_reply)
|
||||
else
|
||||
getString(R.string.view_replies_count, comment.replyCount)
|
||||
}
|
||||
repliesVisible = false
|
||||
} else {
|
||||
commentTotalReplies.setText(R.string.hide_replies)
|
||||
repliesSection.clear()
|
||||
commentsFragment.viewReplyCallback(item)
|
||||
repliesVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewBinding.commentUserName.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
.putExtra("userId", comment.userId.toInt())
|
||||
.putExtra("userLVL","[${levelColor.second}]"), null
|
||||
)
|
||||
}
|
||||
viewBinding.commentUserAvatar.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
.putExtra("userId", comment.userId.toInt())
|
||||
.putExtra("userLVL","[${levelColor.second}]"), null
|
||||
)
|
||||
}
|
||||
viewBinding.commentText.setOnLongClickListener {
|
||||
copyToClipboard(comment.content)
|
||||
true
|
||||
}
|
||||
commentUserName.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
commentsFragment.activity,
|
||||
Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
.putExtra("userId", comment.userId.toInt()),
|
||||
null
|
||||
)
|
||||
}
|
||||
commentUserAvatar.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
commentsFragment.activity,
|
||||
Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
.putExtra("userId", comment.userId.toInt()),
|
||||
null
|
||||
)
|
||||
}
|
||||
commentText.setOnLongClickListener {
|
||||
copyToClipboard(comment.content)
|
||||
true
|
||||
}
|
||||
|
||||
viewBinding.commentEdit.setOnClickListener {
|
||||
editing(!isEditing)
|
||||
commentsFragment.editCallback(this)
|
||||
}
|
||||
viewBinding.commentReply.setOnClickListener {
|
||||
replying(!isReplying)
|
||||
commentsFragment.replyTo(this, comment.username)
|
||||
commentsFragment.replyCallback(this)
|
||||
}
|
||||
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.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)
|
||||
commentEdit.setOnClickListener {
|
||||
editing(!isEditing)
|
||||
commentsFragment.editCallback(item)
|
||||
}
|
||||
commentReply.setOnClickListener {
|
||||
replying(!isReplying)
|
||||
commentsFragment.replyTo(item, comment.username)
|
||||
commentsFragment.replyCallback(item)
|
||||
}
|
||||
modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
|
||||
adminBadge.visibility =
|
||||
if (comment.isAdmin == true) View.VISIBLE else View.GONE
|
||||
commentInfo.setOnClickListener {
|
||||
val popup = PopupMenu(commentsFragment.requireContext(), 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
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
//fill the icon if the user has liked the comment
|
||||
setVoteButtons(viewBinding)
|
||||
commentUpVote.setOnClickListener {
|
||||
val voteType = if (comment.userVoteType == 1) 0 else 1
|
||||
val previousVoteType = comment.userVoteType
|
||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
scope.launch {
|
||||
val success = CommentsAPI.vote(comment.commentId, voteType)
|
||||
if (success) {
|
||||
comment.userVoteType = voteType
|
||||
|
||||
if (previousVoteType == -1) {
|
||||
comment.downvotes -= 1
|
||||
}
|
||||
comment.upvotes += if (voteType == 1) 1 else -1
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
//fill the icon if the user has liked the comment
|
||||
setVoteButtons(viewBinding)
|
||||
viewBinding.commentUpVote.setOnClickListener {
|
||||
val voteType = if (comment.userVoteType == 1) 0 else 1
|
||||
val previousVoteType = comment.userVoteType
|
||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
scope.launch {
|
||||
val success = CommentsAPI.vote(comment.commentId, voteType)
|
||||
if (success) {
|
||||
comment.userVoteType = voteType
|
||||
|
||||
if (previousVoteType == -1) {
|
||||
comment.downvotes -= 1
|
||||
commentDownVote.setOnClickListener {
|
||||
val voteType = if (comment.userVoteType == -1) 0 else -1
|
||||
val previousVoteType = comment.userVoteType
|
||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
scope.launch {
|
||||
val success = CommentsAPI.vote(comment.commentId, voteType)
|
||||
if (success) {
|
||||
comment.userVoteType = voteType
|
||||
if (previousVoteType == 1) {
|
||||
comment.upvotes -= 1
|
||||
}
|
||||
comment.downvotes += if (voteType == -1) 1 else -1
|
||||
notifyChanged()
|
||||
}
|
||||
comment.upvotes += if (voteType == 1) 1 else -1
|
||||
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewBinding.commentDownVote.setOnClickListener {
|
||||
val voteType = if (comment.userVoteType == -1) 0 else -1
|
||||
val previousVoteType = comment.userVoteType
|
||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
scope.launch {
|
||||
val success = CommentsAPI.vote(comment.commentId, voteType)
|
||||
if (success) {
|
||||
comment.userVoteType = voteType
|
||||
|
||||
if (previousVoteType == 1) {
|
||||
comment.upvotes -= 1
|
||||
}
|
||||
comment.downvotes += if (voteType == -1) 1 else -1
|
||||
notifyChanged()
|
||||
}
|
||||
commentTotalVotes.text = (comment.upvotes - comment.downvotes).toString()
|
||||
commentUserAvatar.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
commentsFragment.activity,
|
||||
commentsFragment.activity.getString(R.string.avatar, comment.username),
|
||||
comment.profilePictureUrl
|
||||
)
|
||||
}
|
||||
comment.profilePictureUrl?.let { commentUserAvatar.loadImage(it) }
|
||||
commentUserName.text = comment.username
|
||||
val userColor = "[${levelColor.second}]"
|
||||
commentUserLevel.text = userColor
|
||||
commentUserLevel.setTextColor(levelColor.first)
|
||||
commentUserTime.text = formatTimestamp(comment.timestamp)
|
||||
}
|
||||
viewBinding.commentTotalVotes.text = (comment.upvotes - comment.downvotes).toString()
|
||||
viewBinding.commentUserAvatar.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
commentsFragment.activity,
|
||||
"${comment.username}'s [Cover]",
|
||||
comment.profilePictureUrl
|
||||
)
|
||||
}
|
||||
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
|
||||
viewBinding.commentUserName.text = comment.username
|
||||
val userColor = "[${levelColor.second}]"
|
||||
viewBinding.commentUserLevel.text = userColor
|
||||
viewBinding.commentUserLevel.setTextColor(levelColor.first)
|
||||
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
|
||||
}
|
||||
|
||||
override fun getLayout(): Int {
|
||||
@@ -273,12 +280,16 @@ class CommentItem(
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -286,7 +297,7 @@ class CommentItem(
|
||||
subCommentIds.add(id)
|
||||
}
|
||||
|
||||
private fun removeSubCommentIds(){
|
||||
private fun removeSubCommentIds() {
|
||||
subCommentIds.forEach { id ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
|
||||
@@ -306,11 +317,13 @@ class CommentItem(
|
||||
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)
|
||||
@@ -355,7 +368,8 @@ class CommentItem(
|
||||
|
||||
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) {
|
||||
@@ -373,16 +387,17 @@ class CommentItem(
|
||||
* @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)
|
||||
}
|
||||
|
||||
@@ -75,7 +75,10 @@ class CommentsFragment : Fragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
activity = requireActivity() as MediaDetailsActivity
|
||||
|
||||
binding.commentsListContainer.setBaseline(activity.navBar, activity.binding.commentInputLayout)
|
||||
binding.commentsListContainer.setBaseline(
|
||||
activity.navBar,
|
||||
activity.binding.commentInputLayout
|
||||
)
|
||||
|
||||
//get the media id from the intent
|
||||
val mediaId = arguments?.getInt("mediaId") ?: -1
|
||||
@@ -118,7 +121,6 @@ class CommentsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast("Not logged in")
|
||||
activity.binding.commentMessageContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -301,7 +303,7 @@ class CommentsFragment : Fragment() {
|
||||
|
||||
activity.binding.commentLabel.setOnClickListener {
|
||||
//alert dialog to enter a number, with a cancel and ok button
|
||||
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
.setTitle("Enter a chapter/episode number tag")
|
||||
.setView(R.layout.dialog_edittext)
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
@@ -577,7 +579,7 @@ class CommentsFragment : Fragment() {
|
||||
* Called when the user tries to comment for the first time
|
||||
*/
|
||||
private fun showCommentRulesDialog() {
|
||||
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
.setTitle("Commenting Rules")
|
||||
.setMessage(
|
||||
"I WILL BAN YOU WITHOUT HESITATION\n" +
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -17,6 +16,7 @@ import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -145,8 +145,6 @@ class MangaChapterAdapter(
|
||||
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private val activeCoroutines = mutableSetOf<String>()
|
||||
private val typedValue1 = TypedValue()
|
||||
private val typedValue2 = TypedValue()
|
||||
fun bind(chapterNumber: String, progress: String?) {
|
||||
if (progress != null) {
|
||||
binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
@@ -201,17 +199,6 @@ class MangaChapterAdapter(
|
||||
}
|
||||
|
||||
init {
|
||||
val theme = currContext()?.theme
|
||||
theme?.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorError,
|
||||
typedValue1,
|
||||
true
|
||||
)
|
||||
theme?.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorPrimary,
|
||||
typedValue2,
|
||||
true
|
||||
)
|
||||
itemView.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
||||
@@ -223,16 +210,15 @@ class MangaChapterAdapter(
|
||||
fragment.onMangaChapterStopDownloadClick(chapterNumber)
|
||||
return@setOnClickListener
|
||||
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
builder.setTitle("Delete Chapter")
|
||||
builder.setMessage("Are you sure you want to delete ${chapterNumber}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
|
||||
it.context.customAlertDialog().apply {
|
||||
setTitle("Delete Chapter")
|
||||
setMessage("Are you sure you want to delete ${chapterNumber}?")
|
||||
setPosButton(R.string.delete) {
|
||||
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
|
||||
}
|
||||
setNegButton(R.string.cancel)
|
||||
show()
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
}
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||
@@ -242,20 +228,20 @@ class MangaChapterAdapter(
|
||||
}
|
||||
binding.itemDownload.setOnLongClickListener {
|
||||
//Alert dialog asking for the number of chapters to download
|
||||
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
alertDialog.setTitle("Multi Chapter Downloader")
|
||||
alertDialog.setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
input.minValue = 1
|
||||
input.maxValue = itemCount - bindingAdapterPosition
|
||||
input.value = 1
|
||||
alertDialog.setView(input)
|
||||
alertDialog.setPositiveButton("OK") { _, _ ->
|
||||
downloadNChaptersFrom(bindingAdapterPosition, input.value)
|
||||
it.context.customAlertDialog().apply {
|
||||
setTitle("Multi Chapter Downloader")
|
||||
setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
input.minValue = 1
|
||||
input.maxValue = itemCount - bindingAdapterPosition
|
||||
input.value = 1
|
||||
setCustomView(input)
|
||||
setPosButton("OK") {
|
||||
downloadNChaptersFrom(bindingAdapterPosition, input.value)
|
||||
}
|
||||
setNegButton("Cancel")
|
||||
show()
|
||||
}
|
||||
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
|
||||
val dialog = alertDialog.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -343,6 +329,7 @@ class MangaChapterAdapter(
|
||||
fun updateType(t: Int) {
|
||||
type = t
|
||||
}
|
||||
|
||||
private fun formatDate(timestamp: Long?): String {
|
||||
timestamp ?: return "" // Return empty string if timestamp is null
|
||||
|
||||
@@ -366,6 +353,7 @@ class MangaChapterAdapter(
|
||||
else -> "Just now"
|
||||
}
|
||||
}
|
||||
|
||||
1L -> "1 day ago"
|
||||
in 2..6 -> "$daysDifference days ago"
|
||||
else -> SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(targetDate)
|
||||
|
||||
@@ -102,6 +102,9 @@ class MangaReadAdapter(
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
}
|
||||
}
|
||||
media.selected?.scanlators?.let {
|
||||
hiddenScanlators.addAll(it)
|
||||
}
|
||||
binding.animeSource.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
@@ -127,7 +130,7 @@ class MangaReadAdapter(
|
||||
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
ext.sourceLanguage = i
|
||||
fragment.onLangChange(i)
|
||||
fragment.onLangChange(i, ext.saveName)
|
||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener =
|
||||
@@ -229,7 +232,7 @@ class MangaReadAdapter(
|
||||
refresh = true
|
||||
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
ContextCompat.startActivity(fragment.requireContext(), intent, null)
|
||||
startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,8 +261,10 @@ class MangaReadAdapter(
|
||||
dialogBinding.animeScanlatorContainer.isVisible = options.count() > 1
|
||||
dialogBinding.scanlatorNo.text = "${options.count()}"
|
||||
dialogBinding.animeScanlatorTop.setOnClickListener {
|
||||
val dialogView2 = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer = dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||
val dialogView2 =
|
||||
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer =
|
||||
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||
val tickAllButton = dialogView2.findViewById<ImageButton>(R.id.toggleButton)
|
||||
|
||||
// Function to get the right image resource for the toggle button
|
||||
@@ -441,7 +446,11 @@ class MangaReadAdapter(
|
||||
if (media.manga?.chapters != null) {
|
||||
val chapters = media.manga.chapters!!.keys.toTypedArray()
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp = PrefManager.getNullableCustomVal("${media.id}_current_chp", null, String::class.java)
|
||||
val appEp = PrefManager.getNullableCustomVal(
|
||||
"${media.id}_current_chp",
|
||||
null,
|
||||
String::class.java
|
||||
)
|
||||
?.toIntOrNull() ?: 1
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
val filteredChapters = chapters.filter { chapterKey ->
|
||||
@@ -470,7 +479,11 @@ class MangaReadAdapter(
|
||||
val ep = media.manga.chapters!![continueEp]!!
|
||||
binding.itemEpisodeImage.loadImage(media.banner ?: media.cover)
|
||||
binding.animeSourceContinueText.text =
|
||||
currActivity()!!.getString(R.string.continue_chapter, ep.number, if (!ep.title.isNullOrEmpty()) ep.title else "")
|
||||
currActivity()!!.getString(
|
||||
R.string.continue_chapter,
|
||||
ep.number,
|
||||
if (!ep.title.isNullOrEmpty()) ep.title else ""
|
||||
)
|
||||
binding.animeSourceContinue.setOnClickListener {
|
||||
fragment.onMangaChapterClick(continueEp)
|
||||
}
|
||||
@@ -491,11 +504,14 @@ class MangaReadAdapter(
|
||||
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources)) {
|
||||
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)
|
||||
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 } }
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
setLanguageList(0, nextIndex)
|
||||
}
|
||||
subscribeButton(false)
|
||||
@@ -532,7 +548,7 @@ class MangaReadAdapter(
|
||||
val adapter = ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
|
||||
parser.extension.sources.map { LanguageMapper.getLanguageName(it.lang) }
|
||||
)
|
||||
val items = adapter.count
|
||||
binding?.animeSourceLanguageContainer?.isVisible = items > 1
|
||||
|
||||
@@ -16,7 +16,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -35,15 +34,16 @@ 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.findValidName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||
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.MediaType
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
@@ -194,8 +194,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
)
|
||||
|
||||
for (download in downloadManager.mangaDownloadedTypes) {
|
||||
if (download.title == media.mainName().findValidName()) {
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
if (media.compareName(download.titleName)) {
|
||||
chapterAdapter.stopDownload(download.chapterName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
ConcatAdapter(headerAdapter, chapterAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val offline =
|
||||
!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
||||
if (offline) media.selected!!.sourceIndex =
|
||||
model.mangaReadSources!!.list.lastIndex
|
||||
model.loadMangaChapters(media, media.selected!!.sourceIndex)
|
||||
}
|
||||
loaded = true
|
||||
@@ -317,11 +321,12 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
return model.mangaReadSources?.get(i)!!
|
||||
}
|
||||
|
||||
fun onLangChange(i: Int) {
|
||||
fun onLangChange(i: Int, saveName: String) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.langIndex = i
|
||||
model.saveSelected(media.id, selected)
|
||||
media.selected = selected
|
||||
PrefManager.removeCustomVal("${saveName}_${media.id}")
|
||||
}
|
||||
|
||||
fun onScanlatorChange(list: List<String>) {
|
||||
@@ -380,7 +385,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names =
|
||||
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
|
||||
allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray()
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, -1) { dialog, which ->
|
||||
@@ -491,7 +496,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
if (success) {
|
||||
continueDownload()
|
||||
} else {
|
||||
snackString("Permission is required to download")
|
||||
snackString(getString(R.string.download_permission_required))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -42,7 +42,8 @@ abstract class BaseImageAdapter(
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
images = if (settings.layout == CurrentReaderSettings.Layouts.PAGED
|
||||
&& settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) {
|
||||
&& settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP
|
||||
) {
|
||||
chapterImages.reversed()
|
||||
} else {
|
||||
chapterImages
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user