mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-17 15:53:56 +00:00
Compare commits
8 Commits
l10n_dev_c
...
custom-dow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61406ebf2c | ||
|
|
33ed647832 | ||
|
|
79113cb04a | ||
|
|
93172153b7 | ||
|
|
9e6f71feb2 | ||
|
|
bf73bbb98b | ||
|
|
fb903c56aa | ||
|
|
bcbcf1a829 |
25
.github/workflows/beta.yml
vendored
25
.github/workflows/beta.yml
vendored
@@ -48,11 +48,9 @@ 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
|
||||
@@ -77,7 +75,7 @@ jobs:
|
||||
|
||||
- name: List files in the directory
|
||||
run: ls -l
|
||||
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
@@ -85,11 +83,9 @@ 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
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
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
|
||||
@@ -103,7 +99,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: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
#Telegram
|
||||
@@ -117,13 +113,18 @@ jobs:
|
||||
VERSION: ${{ env.VERSION }}
|
||||
|
||||
- name: Upload Current SHA as Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: last-sha
|
||||
path: last_sha.txt
|
||||
|
||||
- name: Upload Commit log as Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
- name: Delete Old Pre-Releases
|
||||
id: delete-pre-releases
|
||||
uses: sgpublic/delete-release-action@master
|
||||
with:
|
||||
name: commit-log
|
||||
path: commit_log.txt
|
||||
pre-release-drop: true
|
||||
pre-release-keep-count: 3
|
||||
pre-release-drop-tag: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,6 +31,3 @@ output.json
|
||||
|
||||
#other
|
||||
scripts/
|
||||
|
||||
#crowdin
|
||||
crowdin.yml
|
||||
@@ -21,7 +21,14 @@ 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"
|
||||
@@ -81,9 +88,9 @@ android {
|
||||
dependencies {
|
||||
|
||||
// FireBase
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.8.1')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.6.2'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.4'
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.7.4')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
|
||||
// Core
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.browser:browser:1.8.0'
|
||||
@@ -111,7 +118,7 @@ dependencies {
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.3.1'
|
||||
ext.exo_version = '1.3.0'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
@@ -131,17 +138,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:7.2.1'
|
||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc'
|
||||
|
||||
// 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'
|
||||
@@ -151,6 +158,9 @@ 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,18 +85,13 @@ object AppUpdater {
|
||||
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
@@ -116,25 +111,24 @@ 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()
|
||||
}
|
||||
|
||||
val new = toDouble(version.split("."))
|
||||
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
||||
new > curr
|
||||
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("."))
|
||||
return new > curr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<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" />
|
||||
@@ -40,17 +38,6 @@
|
||||
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" />
|
||||
@@ -92,7 +79,7 @@
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.statistics.ProfileStatsWidget"
|
||||
android:exported="false">
|
||||
@@ -136,33 +123,6 @@
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAboutActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAccountActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAnimeActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsCommonActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsExtensionsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAddonActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsMangaActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsNotificationActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsThemeActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.ExtensionsActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
@@ -171,7 +131,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>
|
||||
|
||||
@@ -434,11 +394,6 @@
|
||||
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".addons.torrent.ServerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
|
||||
@@ -6,8 +6,6 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
@@ -43,9 +41,6 @@ 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)
|
||||
@@ -101,8 +96,6 @@ class App : MultiDexApplication() {
|
||||
animeExtensionManager = Injekt.get()
|
||||
mangaExtensionManager = Injekt.get()
|
||||
novelExtensionManager = Injekt.get()
|
||||
torrentAddonManager = Injekt.get()
|
||||
downloadAddonManager = Injekt.get()
|
||||
|
||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
@@ -122,11 +115,6 @@ class App : MultiDexApplication() {
|
||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val addonScope = CoroutineScope(Dispatchers.Default)
|
||||
addonScope.launch {
|
||||
torrentAddonManager.init()
|
||||
downloadAddonManager.init()
|
||||
}
|
||||
val commentsScope = CoroutineScope(Dispatchers.Default)
|
||||
commentsScope.launch {
|
||||
CommentsAPI.fetchAuthToken()
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -67,9 +68,12 @@ import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
@@ -88,7 +92,6 @@ 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
|
||||
@@ -99,7 +102,6 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||
import ani.dantotsu.util.CountUpTimer
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
@@ -132,12 +134,9 @@ 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
|
||||
@@ -185,10 +184,9 @@ fun currActivity(): Activity? {
|
||||
var loadMedia: Int? = null
|
||||
var loadIsMAL = false
|
||||
|
||||
val Int.toPx
|
||||
get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
|
||||
).toInt()
|
||||
val Int.toPx get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
|
||||
).toInt()
|
||||
|
||||
fun initActivity(a: Activity) {
|
||||
val window = a.window
|
||||
@@ -217,8 +215,7 @@ 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())
|
||||
@@ -294,12 +291,7 @@ 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() {
|
||||
@@ -309,19 +301,23 @@ fun Activity.reloadActivity() {
|
||||
initActivity(this)
|
||||
}
|
||||
|
||||
fun Activity.restartApp() {
|
||||
fun Context.restartApp(view: View) {
|
||||
val mainIntent = Intent.makeRestartActivityTask(
|
||||
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
|
||||
)
|
||||
val component =
|
||||
ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
|
||||
try {
|
||||
startActivity(Intent().setComponent(component))
|
||||
} catch (e: Exception) {
|
||||
startActivity(mainIntent)
|
||||
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()
|
||||
}
|
||||
finishAndRemoveTask()
|
||||
PrefManager.setCustomVal("reload", true)
|
||||
}
|
||||
|
||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
@@ -468,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)) {
|
||||
@@ -957,8 +953,7 @@ 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 =
|
||||
@@ -990,50 +985,6 @@ fun countDown(media: Media, view: ViewGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
fun sinceWhen(media: Media, view: ViewGroup) {
|
||||
if (media.status != "RELEASING" && media.status != "HIATUS") return
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
|
||||
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
|
||||
val timeSince = (System.currentTimeMillis() -
|
||||
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
val v =
|
||||
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
|
||||
|
||||
object : CountUpTimer(86400000) {
|
||||
override fun onTick(second: Int) {
|
||||
val a = second + timeSince
|
||||
v.mediaCountdown.text = currActivity()?.getString(
|
||||
R.string.time_format,
|
||||
a / 86400,
|
||||
a % 86400 / 3600,
|
||||
a % 86400 % 3600 / 60,
|
||||
a % 86400 % 3600 % 60
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
// The legend will never die.
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun displayTimer(media: Media, view: ViewGroup) {
|
||||
when {
|
||||
media.anime != null -> countDown(media, view)
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
|
||||
else -> {} // No timer yet
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
|
||||
this.forEach {
|
||||
if (it.value.id == id) {
|
||||
@@ -1350,11 +1301,7 @@ fun blurImage(imageView: ImageView, banner: String?) {
|
||||
if (!(context as Activity).isDestroyed) {
|
||||
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
|
||||
Glide.with(context as Context)
|
||||
.load(
|
||||
if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
|
||||
url
|
||||
) else File(url)
|
||||
)
|
||||
.load(GlideUrl(url))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
|
||||
.into(imageView)
|
||||
@@ -1440,4 +1387,4 @@ fun buildMarkwon(
|
||||
}))
|
||||
.build()
|
||||
return markwon
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -19,6 +20,7 @@ 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
|
||||
@@ -33,14 +35,14 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import ani.dantotsu.addons.torrent.ServerService
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
@@ -68,13 +70,11 @@ 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,7 +87,6 @@ class MainActivity : AppCompatActivity() {
|
||||
private var load = false
|
||||
|
||||
|
||||
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -300,7 +299,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
var launched = false
|
||||
intent.extras?.let { extras ->
|
||||
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
|
||||
val mediaId = extras.getInt("mediaId", -1)
|
||||
@@ -313,7 +311,6 @@ 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 {
|
||||
@@ -321,7 +318,6 @@ class MainActivity : AppCompatActivity() {
|
||||
putExtra("activityId", activityId)
|
||||
|
||||
}
|
||||
launched = true
|
||||
startActivity(feedIntent)
|
||||
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
||||
Logger.log("MainActivity, onCreate: $activityId")
|
||||
@@ -329,7 +325,6 @@ class MainActivity : AppCompatActivity() {
|
||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
||||
putExtra("activityId", activityId)
|
||||
}
|
||||
launched = true
|
||||
startActivity(notificationIntent)
|
||||
}
|
||||
}
|
||||
@@ -383,7 +378,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load && !launched) {
|
||||
if (!load) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMain(this@MainActivity)
|
||||
val id = intent.extras?.getInt("mediaId", 0)
|
||||
@@ -454,26 +449,16 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val torrentManager = Injekt.get<TorrentAddonManager>()
|
||||
fun startTorrent() {
|
||||
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
|
||||
launchIO {
|
||||
if (!ServerService.isRunning()) {
|
||||
ServerService.start()
|
||||
}
|
||||
/*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)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (torrentManager.isInitialized.value == false) {
|
||||
torrentManager.isInitialized.observe(this) {
|
||||
if (it) {
|
||||
startTorrent()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startTorrent()
|
||||
}
|
||||
}*/ //TODO: remove this
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
@@ -484,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)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
abstract class Addon {
|
||||
abstract val name: String
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Long
|
||||
|
||||
abstract class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
) : Addon()
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.settings.InstallerSteps
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
||||
class AddonDownloader {
|
||||
companion object {
|
||||
private suspend fun check(repo: String): Pair<String, String> {
|
||||
return try {
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<AppUpdater.GithubResponse>(it)
|
||||
}
|
||||
val r = res.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
val md = r.body ?: ""
|
||||
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
|
||||
Logger.log("Git Version : $version")
|
||||
Pair(md, version)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error checking for update")
|
||||
Logger.log(e)
|
||||
Pair("", "")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun hasUpdate(repo: String, currentVersion: String): Boolean {
|
||||
val (_, version) = check(repo)
|
||||
return compareVersion(version, currentVersion)
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
activity: Activity,
|
||||
manager: AddonManager<*>,
|
||||
repo: String,
|
||||
currentVersion: String
|
||||
) {
|
||||
val (_, version) = check(repo)
|
||||
if (!compareVersion(version, currentVersion)) {
|
||||
toast(activity.getString(R.string.no_update_found))
|
||||
return
|
||||
}
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
val apks =
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<AppUpdater.GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(
|
||||
".apk"
|
||||
)
|
||||
}
|
||||
val apkToDownload =
|
||||
apks?.find { it.browserDownloadURL.contains(getCurrentABI()) }
|
||||
?: apks?.find { it.browserDownloadURL.contains("universal") }
|
||||
?: apks?.first()
|
||||
apkToDownload?.browserDownloadURL.apply {
|
||||
if (this != null) {
|
||||
val notificationManager =
|
||||
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val installerSteps = InstallerSteps(notificationManager, activity)
|
||||
manager.install(this)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||
{ error -> installerSteps.onError(error) {} },
|
||||
{ installerSteps.onComplete {} }
|
||||
)
|
||||
} else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ABI that the app is most likely running on.
|
||||
* @return The primary ABI for the device.
|
||||
*/
|
||||
private fun getCurrentABI(): String {
|
||||
return if (Build.SUPPORTED_ABIS.isNotEmpty()) {
|
||||
Build.SUPPORTED_ABIS[0]
|
||||
} else "Unknown"
|
||||
}
|
||||
|
||||
private fun compareVersion(newVersion: String, oldVersion: String): Boolean {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return try {
|
||||
list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
} catch (e: NumberFormatException) {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
val new = toDouble(newVersion.split("."))
|
||||
val curr = toDouble(oldVersion.split("."))
|
||||
return new > curr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
interface AddonListener {
|
||||
fun onAddonInstalled(result: LoadResult?)
|
||||
fun onAddonUpdated(result: LoadResult?)
|
||||
fun onAddonUninstalled(pkgName: String)
|
||||
|
||||
enum class ListenerAction {
|
||||
INSTALL, UPDATE, UNINSTALL
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import ani.dantotsu.addons.download.DownloadAddon
|
||||
import ani.dantotsu.addons.download.DownloadAddonApi
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.download.DownloadLoadResult
|
||||
import ani.dantotsu.addons.torrent.TorrentAddon
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonApi
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentLoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
|
||||
class AddonLoader {
|
||||
companion object {
|
||||
fun loadExtension(
|
||||
context: Context,
|
||||
packageName: String,
|
||||
className: String,
|
||||
type: AddonType
|
||||
): LoadResult? {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(ExtensionLoader.PACKAGE_FLAGS.toLong()))
|
||||
} else {
|
||||
pkgManager.getInstalledPackages(ExtensionLoader.PACKAGE_FLAGS)
|
||||
}
|
||||
|
||||
val extPkgs = installedPkgs.filter {
|
||||
isPackageAnExtension(
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
if (extPkgs.isEmpty()) return null
|
||||
if (extPkgs.size > 1) throw IllegalStateException("Multiple extensions with the same package name found")
|
||||
|
||||
val pkgName = extPkgs.first().packageName
|
||||
val pkgInfo = extPkgs.first()
|
||||
|
||||
val appInfo = try {
|
||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||
} catch (error: PackageManager.NameNotFoundException) {
|
||||
// Unlikely, but the package may have been uninstalled at this point
|
||||
Logger.log(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
val extName =
|
||||
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Dantotsu: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
Logger.log("Missing versionName for extension $extName")
|
||||
throw IllegalStateException("Missing versionName for extension $extName")
|
||||
}
|
||||
val classLoader =
|
||||
PathClassLoader(appInfo.sourceDir, appInfo.nativeLibraryDir, context.classLoader)
|
||||
val loadedClass = try {
|
||||
Class.forName(className, false, classLoader)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
}
|
||||
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> {
|
||||
val extension = instance as? TorrentAddonApi
|
||||
?: throw IllegalStateException("Extension is not a TorrentAddonApi")
|
||||
TorrentLoadResult.Success(
|
||||
TorrentAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
AddonType.DOWNLOAD -> {
|
||||
val extension = instance as? DownloadAddonApi
|
||||
?: throw IllegalStateException("Extension is not a DownloadAddonApi")
|
||||
DownloadLoadResult.Success(
|
||||
DownloadAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
TorrentAddonManager.TORRENT_CLASS,
|
||||
type
|
||||
)
|
||||
|
||||
AddonType.DOWNLOAD -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
DownloadAddonManager.DOWNLOAD_CLASS,
|
||||
type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
|
||||
return pkgInfo.packageName.equals(type)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import rx.Observable
|
||||
|
||||
abstract class AddonManager<T : Addon.Installed>(
|
||||
private val context: Context
|
||||
) {
|
||||
abstract var extension: T?
|
||||
abstract var name: String
|
||||
abstract var type: AddonType
|
||||
protected val installer by lazy { ExtensionInstaller(context) }
|
||||
var hasUpdate: Boolean = false
|
||||
protected set
|
||||
|
||||
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
|
||||
|
||||
abstract suspend fun init()
|
||||
abstract fun isAvailable(): Boolean
|
||||
abstract fun getVersion(): String?
|
||||
abstract fun getPackageName(): String?
|
||||
abstract fun hadError(context: Context): String?
|
||||
abstract fun updateInstallStep(id: Long, step: InstallStep)
|
||||
abstract fun setInstalling(id: Long)
|
||||
|
||||
fun uninstall() {
|
||||
getPackageName()?.let {
|
||||
installer.uninstallApk(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun addListenerAction(action: (AddonListener.ListenerAction) -> Unit) {
|
||||
onListenerAction = action
|
||||
}
|
||||
|
||||
fun removeListenerAction() {
|
||||
onListenerAction = null
|
||||
}
|
||||
|
||||
fun install(url: String): Observable<InstallStep> {
|
||||
return installer.downloadAndInstall(url, getPackageName() ?: "", name, type)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
abstract class LoadResult {
|
||||
|
||||
abstract class Success : LoadResult()
|
||||
|
||||
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.filter
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.getPackageNameFromIntent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
|
||||
internal class AddonInstallReceiver : BroadcastReceiver() {
|
||||
private var listener: AddonListener? = null
|
||||
private var type: AddonType? = null
|
||||
|
||||
/**
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
fun setListener(listener: AddonListener, type: AddonType): AddonInstallReceiver {
|
||||
this.listener = listener
|
||||
this.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the events of the [filter] is received. When the package is an extension,
|
||||
* it's loaded in background and it notifies the [listener] when finished.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_PACKAGE_ADDED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class DownloadAddon : Addon() {
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: DownloadAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
interface DownloadAddonApi {
|
||||
|
||||
fun cancelDownload(sessionId: Long)
|
||||
|
||||
fun setDownloadPath(context: Context, uri: Uri): String
|
||||
|
||||
suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit)
|
||||
|
||||
suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long
|
||||
|
||||
fun getState(sessionId: Long): String
|
||||
|
||||
fun getStackTrace(sessionId: Long): String?
|
||||
|
||||
fun hadError(sessionId: Long): Boolean
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<DownloadAddon.Installed>(context) {
|
||||
|
||||
override var extension: DownloadAddon.Installed? = null
|
||||
override var name: String = "Download Addon"
|
||||
override var type = AddonType.DOWNLOAD
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
DOWNLOAD_PACKAGE,
|
||||
DOWNLOAD_CLASS,
|
||||
AddonType.DOWNLOAD
|
||||
) as? DownloadLoadResult
|
||||
result?.let {
|
||||
if (it is DownloadLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing Download extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (extension?.pkgName == pkgName) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val DOWNLOAD_PACKAGE = "dantotsu.downloadAddon"
|
||||
const val DOWNLOAD_CLASS = "ani.dantotsu.downloadAddon.DownloadAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Download-Addon"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class DownloadLoadResult : LoadResult() {
|
||||
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class TorrentAddon : Addon() {
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: TorrentAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
|
||||
|
||||
interface TorrentAddonApi {
|
||||
|
||||
fun startServer(path: String)
|
||||
|
||||
fun stopServer()
|
||||
|
||||
fun echo(): String
|
||||
|
||||
fun removeTorrent(torrent: String)
|
||||
|
||||
fun addTorrent(
|
||||
link: String,
|
||||
title: String,
|
||||
poster: String,
|
||||
data: String,
|
||||
save: Boolean,
|
||||
): Torrent
|
||||
|
||||
fun getLink(torrent: Torrent, index: Int): String
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.addons.download.AddonInstallReceiver
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TorrentAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<TorrentAddon.Installed>(context) {
|
||||
override var extension: TorrentAddon.Installed? = null
|
||||
override var name: String = "Torrent Addon"
|
||||
override var type: AddonType = AddonType.TORRENT
|
||||
var torrentHash: String? = null
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
Logger.log("Torrent extension is not supported on this device.")
|
||||
error = context.getString(R.string.torrent_extension_not_supported)
|
||||
return
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
TORRENT_PACKAGE,
|
||||
TORRENT_CLASS,
|
||||
type
|
||||
) as TorrentLoadResult?
|
||||
result?.let {
|
||||
if (it is TorrentLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing torrent extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (pkgName == TORRENT_PACKAGE) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TORRENT_PACKAGE = "dantotsu.torrentAddon"
|
||||
const val TORRENT_CLASS = "ani.dantotsu.torrentAddon.TorrentAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Torrent-Addon"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class TorrentLoadResult : LoadResult() {
|
||||
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.ID_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
|
||||
class ServerService : Service() {
|
||||
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
|
||||
private val applicationContext = Injekt.get<Application>()
|
||||
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
intent?.let {
|
||||
if (it.action != null) {
|
||||
when (it.action) {
|
||||
ACTION_START -> {
|
||||
startServer()
|
||||
notification(applicationContext)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
ACTION_STOP -> {
|
||||
stopServer()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startServer() {
|
||||
serviceScope.launch {
|
||||
val echo = extension.echo()
|
||||
if (echo == "") {
|
||||
extension.startServer(filesDir.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopServer() {
|
||||
serviceScope.launch {
|
||||
extension.stopServer()
|
||||
applicationContext.cancelNotification(ID_TORRENT_SERVER)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notification(context: Context) {
|
||||
val exitPendingIntent =
|
||||
PendingIntent.getService(
|
||||
applicationContext,
|
||||
0,
|
||||
Intent(applicationContext, ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val builder = context.notificationBuilder(CHANNEL_TORRENT_SERVER) {
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setContentText("Torrent Server")
|
||||
setContentTitle("Server is running…")
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setUsesChronometer(true)
|
||||
addAction(
|
||||
R.drawable.ic_circle_cancel,
|
||||
"Stop",
|
||||
exitPendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
ID_TORRENT_SERVER,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
} else {
|
||||
startForeground(ID_TORRENT_SERVER, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_START = "start_torrent_server"
|
||||
const val ACTION_STOP = "stop_torrent_server"
|
||||
|
||||
fun isRunning(): Boolean {
|
||||
with(Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
|
||||
@Suppress("DEPRECATION") // We only need our services
|
||||
getRunningServices(Int.MAX_VALUE).forEach {
|
||||
if (ServerService::class.java.name.equals(it.service.className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun start() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_START
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun stop() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun wait(timeout: Int = -1): Boolean {
|
||||
var count = 0
|
||||
if (timeout < 0) {
|
||||
count = -20
|
||||
}
|
||||
var echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
while (echo == "") {
|
||||
Thread.sleep(1000)
|
||||
count++
|
||||
if (count > timeout) {
|
||||
return false
|
||||
}
|
||||
echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
}
|
||||
Logger.log("ServerService: Server started: $echo")
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.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
|
||||
@@ -40,13 +38,10 @@ 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,6 +3,7 @@ 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
|
||||
@@ -13,6 +14,7 @@ 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
|
||||
|
||||
@@ -199,9 +201,7 @@ object Anilist {
|
||||
toast("Rate limited. Try after $retry seconds")
|
||||
throw Exception("Rate limited after $retry seconds")
|
||||
}
|
||||
if (!json.text.startsWith("{")) {
|
||||
throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
}
|
||||
if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))}
|
||||
if (show) Logger.log("Anilist Response: ${json.text}")
|
||||
json.parsed()
|
||||
} else null
|
||||
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -73,14 +72,14 @@ class AnilistQueries {
|
||||
media.cameFromContinue = false
|
||||
|
||||
val query =
|
||||
"""{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
|
||||
"""{Media(id:${media.id}){id 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}}}"""
|
||||
runBlocking {
|
||||
val anilist = async {
|
||||
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
||||
if (response != null) {
|
||||
fun parse() {
|
||||
val fetchedMedia = response?.data?.media ?: return
|
||||
val user = response?.data?.page
|
||||
|
||||
media.source = fetchedMedia.source?.toString()
|
||||
media.countryOfOrigin = fetchedMedia.countryOfOrigin
|
||||
media.format = fetchedMedia.format?.toString()
|
||||
@@ -140,15 +139,7 @@ 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>
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -162,7 +153,7 @@ class AnilistQueries {
|
||||
Author(
|
||||
id = id,
|
||||
name = i.node?.name?.userPreferred,
|
||||
image = i.node?.image?.large,
|
||||
image = i.node?.image?.medium,
|
||||
role = when (i.role.toString()) {
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||
?: "MAIN"
|
||||
@@ -209,24 +200,7 @@ class AnilistQueries {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (user?.mediaList?.isNotEmpty() == true) {
|
||||
media.users = user.mediaList?.mapNotNull {
|
||||
it.user?.let { user ->
|
||||
if (user.id != Anilist.userid) {
|
||||
User(
|
||||
user.id,
|
||||
user.name ?: "Unknown",
|
||||
user.avatar?.large,
|
||||
"",
|
||||
it.status?.toString(),
|
||||
it.score,
|
||||
it.progress,
|
||||
fetchedMedia.episodes ?: fetchedMedia.chapters,
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}?.toCollection(arrayListOf()) ?: arrayListOf()
|
||||
}
|
||||
|
||||
if (fetchedMedia.mediaListEntry != null) {
|
||||
fetchedMedia.mediaListEntry?.apply {
|
||||
media.userProgress = progress
|
||||
@@ -1061,157 +1035,63 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private val onListAnime =
|
||||
(if (PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace(
|
||||
"\"",
|
||||
""
|
||||
)
|
||||
private val isAdult =
|
||||
(if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
|
||||
|
||||
private fun recentAnimeUpdates(page: Int): String {
|
||||
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
|
||||
private 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 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 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 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 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 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}}}"""
|
||||
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}}}"""
|
||||
}
|
||||
|
||||
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(
|
||||
"\"",
|
||||
""
|
||||
suspend fun loadAnimeList(): Query.AnimeList?{
|
||||
return executeQuery<Query.AnimeList>(
|
||||
"""{
|
||||
recentUpdates:${recentAnimeUpdates()}
|
||||
trendingMovies:${trendingMovies()}
|
||||
topRated:${topRatedAnime()}
|
||||
mostFav:${mostFavAnime()}
|
||||
}""".trimIndent(), force = true
|
||||
)
|
||||
|
||||
private fun trendingManga(page: Int): String {
|
||||
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
|
||||
private fun trendingManhwa(page: Int): String {
|
||||
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
private 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 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 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 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 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 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}}}"""
|
||||
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}}}"""
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
}
|
||||
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) {
|
||||
@@ -1258,26 +1138,25 @@ 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())
|
||||
}
|
||||
next()
|
||||
while (res?.pageInfo?.hasNextPage == true) {
|
||||
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()
|
||||
i++
|
||||
}
|
||||
return list.reversed().toMutableList()
|
||||
while (res?.pageInfo?.hasNextPage == true) {
|
||||
next()
|
||||
i++
|
||||
}
|
||||
return list.reversed().toMutableList()
|
||||
}
|
||||
|
||||
suspend fun getCharacterDetails(character: Character): Character {
|
||||
@@ -1463,39 +1342,19 @@ Page(page:$page,perPage:50) {
|
||||
}
|
||||
}
|
||||
}
|
||||
characters(page: $page,sort:FAVOURITES_DESC) {
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
}
|
||||
nodes{
|
||||
id
|
||||
name {
|
||||
first
|
||||
middle
|
||||
last
|
||||
full
|
||||
native
|
||||
userPreferred
|
||||
}
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
|
||||
var hasNextPage = true
|
||||
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
|
||||
var page = 0
|
||||
val characters = arrayListOf<Character>()
|
||||
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
val query = executeQuery<Query.Author>(
|
||||
query(page), force = true
|
||||
)?.data?.author
|
||||
hasNextPage = query?.staffMedia?.let {
|
||||
hasNextPage = executeQuery<Query.Author>(
|
||||
query(page),
|
||||
force = true
|
||||
)?.data?.author?.staffMedia?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
@@ -1510,20 +1369,6 @@ 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")) {
|
||||
@@ -1531,7 +1376,6 @@ Page(page:$page,perPage:50) {
|
||||
yearMedia.remove("CANCELLED")
|
||||
yearMedia["CANCELLED"] = a
|
||||
}
|
||||
author.character = characters
|
||||
author.yearMedia = yearMedia
|
||||
return author
|
||||
}
|
||||
@@ -1603,11 +1447,7 @@ Page(page:$page,perPage:50) {
|
||||
}
|
||||
|
||||
|
||||
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,}}}}""",
|
||||
@@ -1622,12 +1462,7 @@ 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,"
|
||||
@@ -1656,26 +1491,14 @@ 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,6 +5,8 @@ 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 ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
@@ -187,29 +189,46 @@ 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 list = Anilist.query.loadAnimeList()
|
||||
updated.postValue(list["recentUpdates"])
|
||||
popularMovies.postValue(list["trendingMovies"])
|
||||
topRatedAnime.postValue(list["topRated"])
|
||||
mostFavAnime.postValue(list["mostFav"])
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,35 +303,33 @@ 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 list = Anilist.query.loadMangaList()
|
||||
popularManga.postValue(list["trendingManga"])
|
||||
popularManhwa.postValue(list["trendingManhwa"])
|
||||
popularNovel.postValue(list["trendingNovel"])
|
||||
topRatedManga.postValue(list["topRated"])
|
||||
mostFavManga.postValue(list["mostFav"])
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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,9 +24,7 @@ class Query {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Media")
|
||||
val media: ani.dantotsu.connections.anilist.api.Media?,
|
||||
@SerialName("Page")
|
||||
val page: ani.dantotsu.connections.anilist.api.Page?
|
||||
val media: ani.dantotsu.connections.anilist.api.Media?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,10 +147,8 @@ 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")
|
||||
@@ -161,16 +157,11 @@ 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")
|
||||
@@ -179,18 +170,12 @@ 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")
|
||||
@@ -321,13 +306,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(
|
||||
@@ -338,9 +323,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,
|
||||
@@ -352,13 +337,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(
|
||||
@@ -366,13 +351,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(
|
||||
@@ -384,19 +369,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(
|
||||
@@ -404,7 +389,7 @@ class Query {
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
) : java.io.Serializable
|
||||
): java.io.Serializable
|
||||
|
||||
//----------------------------------------
|
||||
// Statistics
|
||||
@@ -413,12 +398,12 @@ class Query {
|
||||
data class StatisticsResponse(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
): java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
val user: StatisticsUser?
|
||||
) : java.io.Serializable
|
||||
): java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -21,7 +21,6 @@ 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"),
|
||||
}
|
||||
@@ -85,9 +84,9 @@ data class Notification(
|
||||
@SerialName("createdAt")
|
||||
val createdAt: Int,
|
||||
@SerialName("media")
|
||||
val media: Media? = null,
|
||||
val media: ani.dantotsu.connections.anilist.api.Media? = null,
|
||||
@SerialName("user")
|
||||
val user: User? = null,
|
||||
val user: ani.dantotsu.connections.anilist.api.User? = null,
|
||||
@SerialName("message")
|
||||
val message: MessageActivity? = null,
|
||||
@SerialName("activity")
|
||||
|
||||
@@ -93,7 +93,6 @@ 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
|
||||
@@ -102,7 +101,6 @@ data class StaffImage(
|
||||
// The character's image of media at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StaffEdge(
|
||||
var role: String?,
|
||||
|
||||
@@ -111,7 +111,7 @@ data class UserAvatar(
|
||||
|
||||
// The avatar of user at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
) : java.io.Serializable
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStatisticTypes(
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package ani.dantotsu.connections.bakaupdates
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okio.ByteString.Companion.encode
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
class MangaUpdates {
|
||||
|
||||
private val Int?.dateFormat get() = String.format("%02d", this)
|
||||
|
||||
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
|
||||
|
||||
suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
|
||||
return tryWithSuspend {
|
||||
val query = JSONObject().apply {
|
||||
try {
|
||||
put("search", title.encode(Charset.forName("UTF-8")))
|
||||
startDate?.let {
|
||||
put(
|
||||
"start_date",
|
||||
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
|
||||
)
|
||||
}
|
||||
put("include_metadata", true)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
val res = client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
|
||||
coroutineScope {
|
||||
res.results?.map {
|
||||
async(Dispatchers.IO) {
|
||||
Logger.log(it.toString())
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
res.results?.first {
|
||||
it.metadata.series.lastUpdated?.timestamp != null
|
||||
&& (it.metadata.series.latestChapter != null
|
||||
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
|
||||
return results.metadata.series.latestChapter?.let {
|
||||
context.getString(R.string.chapter_number, it)
|
||||
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
|
||||
chapter.takeIf {
|
||||
it.toIntOrNull() == null
|
||||
} ?: context.getString(R.string.chapter_number, chapter.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MangaUpdatesResponse(
|
||||
@SerialName("total_hits")
|
||||
val totalHits: Int?,
|
||||
@SerialName("page")
|
||||
val page: Int?,
|
||||
@SerialName("per_page")
|
||||
val perPage: Int?,
|
||||
val results: List<Results>? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Results(
|
||||
val record: Record,
|
||||
val metadata: MetaData
|
||||
) {
|
||||
@Serializable
|
||||
data class Record(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("title")
|
||||
val title: String,
|
||||
@SerialName("volume")
|
||||
val volume: String?,
|
||||
@SerialName("chapter")
|
||||
val chapter: String?,
|
||||
@SerialName("release_date")
|
||||
val releaseDate: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MetaData(
|
||||
val series: Series
|
||||
) {
|
||||
@Serializable
|
||||
data class Series(
|
||||
@SerialName("series_id")
|
||||
val seriesId: Long?,
|
||||
@SerialName("title")
|
||||
val title: String?,
|
||||
@SerialName("latest_chapter")
|
||||
val latestChapter: Int?,
|
||||
@SerialName("last_updated")
|
||||
val lastUpdated: LastUpdated?
|
||||
) {
|
||||
@Serializable
|
||||
data class LastUpdated(
|
||||
@SerialName("timestamp")
|
||||
val timestamp: Long,
|
||||
@SerialName("as_rfc3339")
|
||||
val asRfc3339: String,
|
||||
@SerialName("as_string")
|
||||
val asString: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,7 @@ 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 {
|
||||
@@ -404,7 +399,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,7 +7,6 @@ class CrashlyticsStub : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun logException(e: Throwable) {
|
||||
Logger.log(e)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,4 @@ 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,8 +402,7 @@ 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 = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(),
|
||||
smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu",
|
||||
smallImage = data.smallImage?.url?.discordUrl(),
|
||||
smallText = data.smallImage?.label
|
||||
),
|
||||
buttons = data.buttons.map { it.label },
|
||||
metadata = Activity.Metadata(
|
||||
|
||||
@@ -22,81 +22,51 @@ class Contributors {
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.forEach {
|
||||
if (it.login == "SunglassJerry") return@forEach
|
||||
val role = when (it.login) {
|
||||
"rebelonion" -> "Owner & Maintainer"
|
||||
"sneazy-ibo" -> "Contributor & Comment Moderator"
|
||||
"WaiWhat" -> "Icon Designer"
|
||||
else -> "Contributor"
|
||||
}
|
||||
res.find { it.login == "rebelonion"}?.let { first ->
|
||||
developers = developers.plus(
|
||||
Developer(
|
||||
it.login,
|
||||
it.avatarUrl,
|
||||
role,
|
||||
it.htmlUrl
|
||||
first.login,
|
||||
first.avatarUrl,
|
||||
"Owner and Maintainer",
|
||||
first.htmlUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
developers = developers.plus(
|
||||
arrayOf(
|
||||
).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 & Website Maintainer",
|
||||
"Beta Icon Designer",
|
||||
"https://github.com/MarshMeadow?tab=repositories"
|
||||
),
|
||||
Developer(
|
||||
"Zaxx69",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6342562-kxE8m4i7KUMK.png",
|
||||
"https://avatars.githubusercontent.com/u/138523882?v=4",
|
||||
"Telegram Admin",
|
||||
"https://anilist.co/user/6342562"
|
||||
"https://github.com/Zaxx69"
|
||||
),
|
||||
Developer(
|
||||
"Arif Alam",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6011177-2n994qtayiR9.jpg",
|
||||
"Discord & Comment Moderator",
|
||||
"https://anilist.co/user/6011177"
|
||||
),
|
||||
"https://avatars.githubusercontent.com/u/70383209?v=4",
|
||||
"Head Discord Moderator",
|
||||
"https://youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
)
|
||||
))
|
||||
}
|
||||
res.filter {it.login != "rebelonion"}.forEach {
|
||||
developers = developers.plus(
|
||||
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"
|
||||
),
|
||||
it.login,
|
||||
it.avatarUrl,
|
||||
"Contributor",
|
||||
it.htmlUrl
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return developers
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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
|
||||
@@ -15,11 +17,10 @@ class Forks {
|
||||
fun getForks(): Array<Developer> {
|
||||
var forks = arrayOf<Developer>()
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val res =
|
||||
client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks?sort=stargazers")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
val res = client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.forEach {
|
||||
forks = forks.plus(
|
||||
Developer(
|
||||
|
||||
@@ -3,7 +3,7 @@ package ani.dantotsu.download
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -19,7 +20,6 @@ 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,14 +53,10 @@ class DownloadsManager(private val context: Context) {
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(
|
||||
downloadedType: DownloadedType,
|
||||
toast: Boolean = true,
|
||||
onFinished: () -> Unit
|
||||
) {
|
||||
fun removeDownload(downloadedType: DownloadedType, onFinished: () -> Unit) {
|
||||
downloadsList.remove(downloadedType)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
removeDirectory(downloadedType, toast)
|
||||
removeDirectory(downloadedType)
|
||||
withContext(Dispatchers.Main) {
|
||||
onFinished()
|
||||
}
|
||||
@@ -131,12 +127,7 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun moveDownloadsDir(
|
||||
context: Context,
|
||||
oldUri: Uri,
|
||||
newUri: Uri,
|
||||
finished: (Boolean, String) -> Unit
|
||||
) {
|
||||
fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) {
|
||||
try {
|
||||
if (oldUri == newUri) {
|
||||
finished(false, "Source and destination are the same")
|
||||
@@ -150,41 +141,18 @@ class DownloadsManager(private val context: Context) {
|
||||
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")
|
||||
@@ -196,7 +164,7 @@ class DownloadsManager(private val context: Context) {
|
||||
super.onCompleted(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
snackString("Error: ${e.message}")
|
||||
@@ -217,16 +185,17 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
|
||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
||||
val directory =
|
||||
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
|
||||
downloadsList.remove(downloadedType)
|
||||
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
if (toast) snackString("Successfully deleted")
|
||||
snackString("Successfully deleted")
|
||||
|
||||
} else {
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
@@ -257,7 +226,11 @@ 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
|
||||
@@ -265,6 +238,7 @@ 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
|
||||
@@ -309,12 +283,7 @@ 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 {
|
||||
@@ -323,19 +292,6 @@ 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? {
|
||||
@@ -347,27 +303,9 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
|
||||
private fun String?.findValidName(): String {
|
||||
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||
}
|
||||
|
||||
data class DownloadedType(
|
||||
val pTitle: String, val pChapter: String, val type: MediaType
|
||||
) : Serializable {
|
||||
|
||||
@@ -19,9 +19,7 @@ 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
|
||||
@@ -34,10 +32,13 @@ 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
|
||||
@@ -73,7 +74,6 @@ 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,11 +82,6 @@ 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 {
|
||||
@@ -168,7 +163,7 @@ class AnimeDownloaderService : Service() {
|
||||
.map { it.sessionId }.toMutableList()
|
||||
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
||||
sessionIds.forEach {
|
||||
ffExtension!!.cancelDownload(it)
|
||||
FFmpegKit.cancel(it)
|
||||
}
|
||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
@@ -203,6 +198,7 @@ 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(
|
||||
@@ -232,44 +228,61 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
var percent = 0
|
||||
var totalLength = 0.0
|
||||
val path = ffExtension!!.setDownloadPath(
|
||||
val path = FFmpegKitConfig.getSafParameterForWrite(
|
||||
this@AnimeDownloaderService,
|
||||
outputFile.uri
|
||||
)
|
||||
val headersStringBuilder = StringBuilder()
|
||||
val headersStringBuilder = StringBuilder().append(" ")
|
||||
task.video.file.headers.forEach {
|
||||
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
|
||||
}
|
||||
if (!task.video.file.headers.containsKey("User-Agent")) { //headers should never be empty now
|
||||
headersStringBuilder.append("\"").append("User-Agent: ")
|
||||
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
|
||||
}
|
||||
val probeRequest =
|
||||
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\""
|
||||
ffExtension.executeFFProbe(
|
||||
probeRequest
|
||||
) {
|
||||
if (it.toDoubleOrNull() != null) {
|
||||
totalLength = it.toDouble()
|
||||
}
|
||||
}
|
||||
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()
|
||||
var request = "-headers $headers "
|
||||
if (task.video.file.headers.isNotEmpty()) {
|
||||
request += headers
|
||||
}
|
||||
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
|
||||
Logger.log("Request: $request")
|
||||
println("Request: $request")
|
||||
val ffTask =
|
||||
ffExtension.executeFFMpeg(request) {
|
||||
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)
|
||||
}) {
|
||||
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||
val timeInMilliseconds = it
|
||||
val timeInMilliseconds = it.time
|
||||
if (timeInMilliseconds > 0 && totalLength > 0) {
|
||||
percent = ((it / 1000) / totalLength * 100).toInt()
|
||||
percent = ((it.time / 1000) / totalLength * 100).toInt()
|
||||
}
|
||||
Logger.log("Statistics: $it")
|
||||
}
|
||||
task.sessionId = ffTask
|
||||
task.sessionId = ffTask.sessionId
|
||||
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
|
||||
ffTask
|
||||
ffTask.sessionId
|
||||
|
||||
saveMediaInfo(task)
|
||||
task.subtitle?.let {
|
||||
@@ -285,8 +298,8 @@ class AnimeDownloaderService : Service() {
|
||||
}
|
||||
|
||||
// periodically check if the download is complete
|
||||
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
||||
if (ffExtension.getState(ffTask) == "FAILED") {
|
||||
while (ffTask.state != SessionState.COMPLETED) {
|
||||
if (ffTask.state == SessionState.FAILED) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
@@ -297,15 +310,14 @@ class AnimeDownloaderService : Service() {
|
||||
} Download failed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
toast("${getTaskName(task.title, task.episode)} Download failed")
|
||||
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
|
||||
snackString("${getTaskName(task.title, task.episode)} Download failed")
|
||||
Logger.log("Download failed: ${ffTask.failStackTrace}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
),
|
||||
false
|
||||
)
|
||||
) {}
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
@@ -333,8 +345,8 @@ class AnimeDownloaderService : Service() {
|
||||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
if (ffExtension.getState(ffTask) == "COMPLETED") {
|
||||
if (ffExtension.hadError(ffTask)) {
|
||||
if (ffTask.state == SessionState.COMPLETED) {
|
||||
if (ffTask.returnCode.isValueError) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
|
||||
@@ -33,7 +33,7 @@ import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
@@ -175,7 +175,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.compareName(item.title) }
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title.findValidName() }
|
||||
media?.let {
|
||||
lifecycleScope.launch {
|
||||
val mediaModel = getMedia(it)
|
||||
|
||||
@@ -30,7 +30,6 @@ import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
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.initActivity
|
||||
import ani.dantotsu.media.Media
|
||||
@@ -169,8 +168,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.compareName(item.title) }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
media?.let {
|
||||
lifecycleScope.launch {
|
||||
ContextCompat.startActivity(
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -49,6 +50,8 @@ 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
|
||||
@@ -267,9 +270,7 @@ 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
|
||||
@@ -357,7 +358,7 @@ class NovelDownloaderService : Service() {
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
launchIO {
|
||||
val directory =
|
||||
getSubDirectory(
|
||||
DownloadsManager.getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
|
||||
@@ -7,23 +7,49 @@ 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
|
||||
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
|
||||
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")
|
||||
object Helper {
|
||||
|
||||
@@ -101,7 +101,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,8 +110,7 @@ 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(
|
||||
@@ -168,8 +167,7 @@ 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 {
|
||||
@@ -197,7 +195,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
fun updateRecent(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
binding.apply{
|
||||
init(
|
||||
adaptor,
|
||||
animeUpdatedRecyclerView,
|
||||
@@ -212,9 +210,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun updateMovies(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
binding.apply{
|
||||
init(
|
||||
adaptor,
|
||||
animeMoviesRecyclerView,
|
||||
@@ -225,7 +222,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
binding.apply{
|
||||
init(
|
||||
adaptor,
|
||||
animeTopRatedRecyclerView,
|
||||
@@ -234,9 +231,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
binding.apply{
|
||||
init(
|
||||
adaptor,
|
||||
animeMostFavRecyclerView,
|
||||
@@ -245,8 +241,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
|
||||
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
@@ -261,7 +256,6 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
if (Anilist.avatar != null && ready.value == true) {
|
||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||
|
||||
@@ -81,10 +81,7 @@ class HomeFragment : Fragment() {
|
||||
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()
|
||||
@@ -131,7 +128,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
|
||||
}
|
||||
@@ -379,7 +376,6 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
|
||||
if (_binding != null) {
|
||||
|
||||
@@ -172,13 +172,7 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
model.getPopularManhwa().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrendingManhwa(
|
||||
MediaAdaptor(
|
||||
0,
|
||||
it,
|
||||
requireActivity()
|
||||
)
|
||||
)
|
||||
mangaPageAdapter.updateTrendingManhwa(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getTopRated().observe(viewLifecycleOwner) {
|
||||
|
||||
@@ -101,7 +101,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,8 +156,7 @@ 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 {
|
||||
@@ -192,7 +191,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
@@ -203,7 +201,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNovel(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
@@ -215,7 +212,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
@@ -226,7 +222,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
@@ -239,8 +234,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
}
|
||||
|
||||
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
|
||||
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
|
||||
@@ -3,10 +3,9 @@ package ani.dantotsu.media
|
||||
import java.io.Serializable
|
||||
|
||||
data class Author(
|
||||
var id: Int,
|
||||
var name: String?,
|
||||
var image: String?,
|
||||
var role: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
|
||||
var character: ArrayList<Character>? = null
|
||||
val id: Int,
|
||||
val name: String?,
|
||||
val image: String?,
|
||||
val role: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
|
||||
) : Serializable
|
||||
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
@@ -33,6 +32,7 @@ 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,15 +55,14 @@ 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
|
||||
@@ -90,19 +89,9 @@ 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]
|
||||
|
||||
@@ -14,6 +14,5 @@ data class Character(
|
||||
var age: String? = null,
|
||||
var gender: String? = null,
|
||||
var dateOfBirth: FuzzyDate? = null,
|
||||
var roles: ArrayList<Media>? = null,
|
||||
val voiceActor: ArrayList<Author>? = null,
|
||||
var roles: ArrayList<Media>? = null
|
||||
) : Serializable
|
||||
@@ -28,7 +28,6 @@ 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,8 +95,7 @@ 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,9 +2,7 @@ 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
|
||||
@@ -29,28 +27,16 @@ 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,7 +7,6 @@ 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
|
||||
|
||||
@@ -67,7 +66,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,
|
||||
|
||||
@@ -100,7 +99,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)
|
||||
@@ -115,8 +114,7 @@ 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,22 +149,14 @@ 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 ?: "??"}"
|
||||
}
|
||||
@@ -191,10 +183,7 @@ 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
|
||||
@@ -243,10 +232,7 @@ 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
|
||||
|
||||
@@ -109,9 +109,7 @@ 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) {
|
||||
@@ -127,11 +125,9 @@ 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
|
||||
@@ -349,13 +345,7 @@ 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,
|
||||
@@ -378,8 +368,7 @@ 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)
|
||||
@@ -423,12 +412,10 @@ 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)
|
||||
}
|
||||
@@ -461,7 +448,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
SupportedMedia.MANGA -> MangaReadFragment()
|
||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val fragment = CommentsFragment()
|
||||
val bundle = Bundle()
|
||||
|
||||
@@ -56,11 +56,9 @@ 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,6 +27,7 @@ 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
|
||||
@@ -37,7 +38,6 @@ import ani.dantotsu.databinding.ItemTitleRecyclerBinding
|
||||
import ani.dantotsu.databinding.ItemTitleSearchBinding
|
||||
import ani.dantotsu.databinding.ItemTitleTextBinding
|
||||
import ani.dantotsu.databinding.ItemTitleTrailerBinding
|
||||
import ani.dantotsu.displayTimer
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
@@ -145,8 +145,7 @@ 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) {
|
||||
@@ -183,9 +182,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) {
|
||||
@@ -214,8 +213,7 @@ 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 {
|
||||
@@ -227,7 +225,8 @@ class MediaInfoFragment : Fragment() {
|
||||
.setDuration(400).start()
|
||||
}
|
||||
}
|
||||
displayTimer(media, binding.mediaInfoContainer)
|
||||
|
||||
countDown(media, binding.mediaInfoContainer)
|
||||
val parent = _binding?.mediaInfoContainer!!
|
||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
|
||||
@@ -572,23 +571,6 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.users.isNullOrEmpty() && !offline) {
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
).apply {
|
||||
itemTitle.setText(R.string.social)
|
||||
itemRecycler.adapter =
|
||||
MediaSocialAdapter(media.users!!)
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemFollowerGridBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.setAnimation
|
||||
|
||||
class MediaSocialAdapter(private val user: ArrayList<User>) :
|
||||
RecyclerView.Adapter<MediaSocialAdapter.DeveloperViewHolder>() {
|
||||
|
||||
inner class DeveloperViewHolder(val binding: ItemFollowerGridBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeveloperViewHolder {
|
||||
return DeveloperViewHolder(
|
||||
ItemFollowerGridBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: DeveloperViewHolder, position: Int) {
|
||||
holder.binding.apply {
|
||||
val user = user[position]
|
||||
val score = user.score?.div(10.0) ?: 0.0
|
||||
setAnimation(root.context, root)
|
||||
profileUserName.text = user.name
|
||||
profileInfo.apply {
|
||||
text = when (user.status) {
|
||||
"CURRENT" -> "WATCHING"
|
||||
else -> user.status ?: ""
|
||||
}
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
profileCompactUserProgress.text = user.progress.toString()
|
||||
profileCompactScore.text = score.toString()
|
||||
profileCompactTotal.text = " | ${user.totalEpisodes ?: "~"}"
|
||||
profileUserAvatar.loadImage(user.pfp)
|
||||
|
||||
val scoreDrawable = if (score == 0.0) R.drawable.score else R.drawable.user_score
|
||||
profileCompactScoreBG.apply {
|
||||
visibility = View.VISIBLE
|
||||
background = ContextCompat.getDrawable(root.context, scoreDrawable)
|
||||
}
|
||||
|
||||
profileCompactProgressContainer.visibility = View.VISIBLE
|
||||
|
||||
profileUserAvatar.setOnClickListener {
|
||||
val intent = Intent(root.context, ProfileActivity::class.java).apply {
|
||||
putExtra("userId", user.id)
|
||||
}
|
||||
ContextCompat.startActivity(root.context, intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = user.size
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
interface Type {
|
||||
fun asText(): String
|
||||
}
|
||||
|
||||
enum class MediaType : Type {
|
||||
enum class MediaType {
|
||||
ANIME,
|
||||
MANGA,
|
||||
NOVEL;
|
||||
|
||||
override fun asText(): String {
|
||||
fun asText(): String {
|
||||
return when (this) {
|
||||
ANIME -> "Anime"
|
||||
MANGA -> "Manga"
|
||||
@@ -18,38 +14,12 @@ enum class MediaType : Type {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string: String): MediaType? {
|
||||
fun fromText(string : String): MediaType {
|
||||
return when (string) {
|
||||
"Anime" -> ANIME
|
||||
"Manga" -> MANGA
|
||||
"Novel" -> NOVEL
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class AddonType : Type {
|
||||
TORRENT,
|
||||
DOWNLOAD;
|
||||
|
||||
override fun asText(): String {
|
||||
return when (this) {
|
||||
TORRENT -> "Torrent"
|
||||
DOWNLOAD -> "Download"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string: String): AddonType? {
|
||||
return when (string) {
|
||||
"Torrent" -> TORRENT
|
||||
"Download" -> DOWNLOAD
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
else -> { ANIME }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +78,8 @@ 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,6 +13,7 @@ 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
|
||||
@@ -59,7 +60,6 @@ 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,42 +129,36 @@ 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()
|
||||
@@ -331,10 +325,7 @@ 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,8 +105,7 @@ 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()
|
||||
@@ -114,8 +113,7 @@ 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()
|
||||
@@ -127,10 +125,8 @@ 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()
|
||||
@@ -210,25 +206,21 @@ 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)
|
||||
@@ -265,8 +257,7 @@ 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(
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class SubtitleDownloader {
|
||||
|
||||
|
||||
@@ -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,7 +330,6 @@ class AnimeWatchAdapter(
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
chip.text = chipText
|
||||
chip.setTextColor(
|
||||
@@ -413,12 +412,10 @@ 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)
|
||||
}
|
||||
@@ -444,14 +441,11 @@ 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)
|
||||
@@ -506,7 +500,8 @@ class AnimeWatchAdapter(
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
displayTimer(media, binding.animeSourceContainer)
|
||||
//Timer
|
||||
countDown(media, binding.animeSourceContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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
|
||||
@@ -24,6 +25,7 @@ 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
|
||||
@@ -33,15 +35,14 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
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.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
|
||||
@@ -199,16 +200,10 @@ class AnimeWatchFragment : Fragment() {
|
||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
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) }
|
||||
)
|
||||
}
|
||||
awaitAll(
|
||||
async { model.loadKitsuEpisodes(media) },
|
||||
async { model.loadFillerEpisodes(media) }
|
||||
)
|
||||
model.loadEpisodes(media, media.selected!!.sourceIndex)
|
||||
}
|
||||
loaded = true
|
||||
@@ -430,27 +425,17 @@ 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(getString(R.string.download_permission_required))
|
||||
snackString("Permission is required to download")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
model.onEpisodeClick(
|
||||
media,
|
||||
i,
|
||||
requireActivity().supportFragmentManager,
|
||||
isDownload = true
|
||||
)
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,6 +472,10 @@ 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)
|
||||
}
|
||||
@@ -553,7 +542,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
for (download in downloadManager.animeDownloadedTypes) {
|
||||
if (media.compareName(download.title)) {
|
||||
if (download.title == media.mainName().findValidName()) {
|
||||
episodeAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ 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,8 +48,6 @@ 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
|
||||
@@ -62,16 +60,12 @@ 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
|
||||
@@ -134,13 +128,13 @@ 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
|
||||
@@ -159,12 +153,10 @@ 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
|
||||
@@ -197,7 +189,6 @@ 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
|
||||
@@ -220,16 +211,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
private var orientationListener: OrientationEventListener? = null
|
||||
|
||||
private var downloadId: String? = null
|
||||
private var hasExtSubtitles = false
|
||||
|
||||
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
|
||||
@@ -297,7 +282,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 + 8.toPx)).dp
|
||||
(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8f.px)).dp
|
||||
exoBrightnessCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
marginEnd =
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
|
||||
@@ -387,31 +372,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
6 -> ResourcesCompat.getFont(this, R.font.blocky)
|
||||
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
|
||||
}
|
||||
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
|
||||
)
|
||||
playerView.subtitleView?.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?) {
|
||||
@@ -444,7 +414,6 @@ 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)
|
||||
@@ -512,8 +481,17 @@ 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)
|
||||
@@ -925,8 +903,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
isFastForwarding = true
|
||||
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
|
||||
fastForward.visibility = View.VISIBLE
|
||||
val speedText = "${exoPlayer.playbackParameters.speed}x"
|
||||
fastForward.text = speedText
|
||||
fastForward.text = "${exoPlayer.playbackParameters.speed}x"
|
||||
}
|
||||
|
||||
fun stopFastForward() {
|
||||
@@ -1219,7 +1196,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
putExtra("subtitle", subtitle)
|
||||
}
|
||||
exoPlayer.pause()
|
||||
onChangeSettings.launch(intent)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
//Speed
|
||||
@@ -1375,6 +1352,7 @@ 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)) {
|
||||
@@ -1391,23 +1369,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
|
||||
//Subtitles
|
||||
hasExtSubtitles = ext.subtitles.isNotEmpty()
|
||||
if (hasExtSubtitles) {
|
||||
exoSubtitle.isVisible = hasExtSubtitles
|
||||
exoSubtitle.setOnClickListener {
|
||||
subClick()
|
||||
}
|
||||
exoSubtitle.isVisible = ext.subtitles.isNotEmpty()
|
||||
exoSubtitle.setOnClickListener {
|
||||
subClick()
|
||||
}
|
||||
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 sub: MediaItem.SubtitleConfiguration? = null
|
||||
if (subtitle != null) {
|
||||
//var localFile: String? = null
|
||||
if (subtitle.type == SubtitleType.UNKNOWN) {
|
||||
if (subtitle?.type == SubtitleType.UNKNOWN) {
|
||||
runBlocking {
|
||||
val type = SubtitleDownloader.loadSubtitleType(subtitleUrl)
|
||||
val fileUri = Uri.parse(subtitleUrl)
|
||||
sub += MediaItem.SubtitleConfiguration
|
||||
val type = SubtitleDownloader.loadSubtitleType(subtitle!!.file.url)
|
||||
val fileUri = Uri.parse(subtitle!!.file.url)
|
||||
sub = MediaItem.SubtitleConfiguration
|
||||
.Builder(fileUri)
|
||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
||||
.setMimeType(
|
||||
@@ -1419,17 +1392,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
)
|
||||
.setId("69")
|
||||
.setLanguage(subtitle.language)
|
||||
.build()
|
||||
}
|
||||
println("sub: $sub")
|
||||
} else {
|
||||
val subUri = Uri.parse(subtitleUrl)
|
||||
sub += MediaItem.SubtitleConfiguration
|
||||
val subUri = Uri.parse(subtitle!!.file.url)
|
||||
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
|
||||
@@ -1437,7 +1409,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
)
|
||||
.setId("69")
|
||||
.setLanguage(subtitle.language)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1479,6 +1450,7 @@ 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()
|
||||
|
||||
@@ -1507,20 +1479,24 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
|
||||
Logger.log("url: ${video!!.file.url}")
|
||||
Logger.log("mimeType: $mimeType")
|
||||
builder.setSubtitleConfigurations(sub)
|
||||
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = listOfNotNull(sub)
|
||||
builder.setSubtitleConfigurations(listofnotnullsubs)
|
||||
}
|
||||
builder.build()
|
||||
} else {
|
||||
if (sub.isNotEmpty()) {
|
||||
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
|
||||
val addLanguage = sub[0].buildUpon().setLanguage("en").build()
|
||||
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = listOfNotNull(sub)
|
||||
val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build()
|
||||
addedSubsDownloadedMediaItem.setSubtitleConfigurations(listOf(addLanguage))
|
||||
episode.selectedSubtitle = 0
|
||||
addedSubsDownloadedMediaItem.build()
|
||||
} else {
|
||||
downloadedMediaItem
|
||||
}
|
||||
addedSubsDownloadedMediaItem.build()
|
||||
}
|
||||
|
||||
|
||||
//Source
|
||||
exoSource.setOnClickListener {
|
||||
sourceClick()
|
||||
@@ -1528,22 +1504,22 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
|
||||
//Quality Track
|
||||
trackSelector = DefaultTrackSelector(this)
|
||||
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)
|
||||
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))
|
||||
)
|
||||
|
||||
if (playbackPosition != 0L && !changingServer && !PrefManager.getVal<Boolean>(PrefName.AlwaysContinue)) {
|
||||
val time = String.format(
|
||||
@@ -1573,18 +1549,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
}.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
if (!this::exoPlayer.isInitialized) buildExoplayer()
|
||||
|
||||
val isDisabled = (subtitle == null && hasExtSubtitles)
|
||||
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(TRACK_TYPE_TEXT, isDisabled)
|
||||
.build()
|
||||
} else buildExoplayer()
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -1615,6 +1589,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
playerView.player = exoPlayer
|
||||
|
||||
|
||||
try {
|
||||
val rightNow = Calendar.getInstance()
|
||||
mediaSession = MediaSession.Builder(this, exoPlayer)
|
||||
@@ -1627,11 +1602,32 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
exoPlayer.addListener(this)
|
||||
exoPlayer.addAnalyticsListener(EventLogger())
|
||||
isInitialized = true
|
||||
|
||||
if (!hasExtSubtitles && !PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
|
||||
onSetTrackGroupOverride(dummyTrack, TRACK_TYPE_TEXT)
|
||||
}
|
||||
}
|
||||
/*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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the track selector parameters to select the subtitle
|
||||
trackSelector.setParameters(parametersBuilder)
|
||||
}*/
|
||||
|
||||
private fun releasePlayer() {
|
||||
isPlayerPlaying = exoPlayer.playWhenReady
|
||||
@@ -1806,8 +1802,7 @@ 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
|
||||
@@ -1830,7 +1825,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
if (new == null) {
|
||||
skipTimeButton.visibility = View.GONE
|
||||
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
|
||||
exoSkip.visibility = View.VISIBLE
|
||||
disappeared = false
|
||||
functionstarted = false
|
||||
cancelTimer()
|
||||
@@ -1839,7 +1834,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
|
||||
override fun onFinish() {
|
||||
skipTimeButton.visibility = View.GONE
|
||||
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
|
||||
exoSkip.visibility = View.VISIBLE
|
||||
disappeared = true
|
||||
functionstarted = false
|
||||
cancelTimer()
|
||||
@@ -1860,7 +1855,6 @@ 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)
|
||||
@@ -1868,13 +1862,6 @@ 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
|
||||
@@ -1889,95 +1876,43 @@ 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\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__: $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()!!
|
||||
}
|
||||
}
|
||||
exoAudioTrack.isVisible = audioTracks.size > 1
|
||||
exoAudioTrack.setOnClickListener {
|
||||
TrackGroupDialogFragment(this, audioTracks, TRACK_TYPE_AUDIO)
|
||||
.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()
|
||||
println("Track: ${tracks.groups.size}")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1987,6 +1922,7 @@ 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()
|
||||
@@ -2041,7 +1977,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeIntentLaunch")
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
finishAndRemoveTask()
|
||||
@@ -2071,11 +2006,10 @@ 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", subtitleUrl)
|
||||
if (subtitle != null) shareVideo.putExtra("subtitle", subtitle!!.file.url)
|
||||
shareVideo.putExtra(
|
||||
"title",
|
||||
media.userPreferredName + " : Ep " + episodeTitleArr[currentEpisodeIndex]
|
||||
@@ -2271,4 +2205,4 @@ class CustomCastButton : MediaRouteButton {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.currActivity
|
||||
@@ -51,12 +50,9 @@ import ani.dantotsu.snackString
|
||||
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
|
||||
@@ -234,12 +230,11 @@ 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")
|
||||
@@ -257,70 +252,32 @@ 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:") || url.endsWith(".torrent")) {
|
||||
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||
if (torrentExtension.isAvailable()) {
|
||||
val activity = currActivity() ?: requireActivity()
|
||||
launchIO {
|
||||
val extension = torrentExtension.extension!!.extension
|
||||
torrentExtension.torrentHash?.let {
|
||||
extension.removeTorrent(it)
|
||||
}
|
||||
val index = if (url.contains("index=")) {
|
||||
url.substringAfter("index=").toIntOrNull() ?: 0
|
||||
} else 0
|
||||
Logger.log("Sending: ${url}, ${video.quality}, $index")
|
||||
val currentTorrent = extension.addTorrent(
|
||||
url, video.quality.toString(), "", "", false
|
||||
)
|
||||
torrentExtension.torrentHash = currentTorrent.hash
|
||||
video.file.url = extension.getLink(currentTorrent, index)
|
||||
Logger.log("Received: ${video.file.url}")
|
||||
if (launch == true) {
|
||||
Intent(activity, ExoplayerView::class.java).apply {
|
||||
ExoplayerView.media = media
|
||||
ExoplayerView.initialized = true
|
||||
startActivity(this)
|
||||
}
|
||||
} else {
|
||||
model.setEpisode(
|
||||
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
|
||||
"startExo no launch"
|
||||
)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
if (url.startsWith("magnet:")) {
|
||||
try {
|
||||
externalPlayerResult.launch(exportMagnetIntent(ep, video))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val amnis = "com.amnis"
|
||||
try {
|
||||
externalPlayerResult.launch(exportMagnetIntent(ep, video))
|
||||
startActivity(Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=$amnis"))
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val amnis = "com.amnis"
|
||||
try {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=$amnis")
|
||||
)
|
||||
)
|
||||
dismiss()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
|
||||
)
|
||||
)
|
||||
}
|
||||
startActivity(Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
|
||||
))
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -328,7 +285,6 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
||||
stopAddingToList()
|
||||
val intent = Intent(activity, ExoplayerView::class.java)
|
||||
@@ -447,11 +403,7 @@ 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -481,37 +433,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
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()
|
||||
selectedVideo?.file?.url?.let { url ->
|
||||
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
||||
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||
if (!torrentExtension.isAvailable()) {
|
||||
snackString("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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val activity = currActivity()?:requireActivity()
|
||||
if (subtitles.isNotEmpty()) {
|
||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Subtitle")
|
||||
@@ -585,13 +510,9 @@ 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,11 +67,7 @@ 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)
|
||||
}
|
||||
@@ -111,11 +107,7 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||
import androidx.media3.common.C.TrackType
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.BottomSheetSubtitlesBinding
|
||||
import ani.dantotsu.databinding.ItemSubtitleTextBinding
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class TrackGroupDialogFragment(
|
||||
instance: ExoplayerView, trackGroups: ArrayList<Tracks.Group>, type: @TrackType Int
|
||||
) : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSubtitlesBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var instance: ExoplayerView
|
||||
private var trackGroups: ArrayList<Tracks.Group>
|
||||
private var type: @TrackType Int
|
||||
|
||||
init {
|
||||
this.instance = instance
|
||||
this.trackGroups = trackGroups
|
||||
this.type = type
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (type == TRACK_TYPE_AUDIO) binding.selectionTitle.text = getString(R.string.audio_tracks)
|
||||
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.subtitlesRecycler.adapter = TrackGroupAdapter()
|
||||
}
|
||||
|
||||
inner class TrackGroupAdapter : RecyclerView.Adapter<TrackGroupAdapter.StreamViewHolder>() {
|
||||
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
|
||||
StreamViewHolder(
|
||||
ItemSubtitleTextBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
trackGroups[position].let { trackGroup ->
|
||||
when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) {
|
||||
null -> {
|
||||
binding.subtitleTitle.text =
|
||||
getString(R.string.unknown_track, "Track $position")
|
||||
}
|
||||
|
||||
"none" -> {
|
||||
binding.subtitleTitle.text = getString(R.string.disabled_track)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val locale = if (language.contains("-")) {
|
||||
val parts = language.split("-")
|
||||
try {
|
||||
Locale(parts[0], parts[1])
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Locale(language)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
binding.subtitleTitle.text = locale?.let {
|
||||
"[${it.language}] ${it.displayName}"
|
||||
|
||||
} ?: getString(R.string.unknown_track, language)
|
||||
}
|
||||
}
|
||||
if (trackGroup.isSelected) {
|
||||
val selected = "✔ ${binding.subtitleTitle.text}"
|
||||
binding.subtitleTitle.text = selected
|
||||
}
|
||||
binding.root.setOnClickListener {
|
||||
dismiss()
|
||||
instance.onSetTrackGroupOverride(trackGroup, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = trackGroups.size
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,7 @@ 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.layoutManager = LinearLayoutManager(commentsFragment.activity)
|
||||
viewBinding.commentRepliesList.adapter = adapter
|
||||
val isUserComment = CommentsAPI.userId == comment.userId
|
||||
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
|
||||
@@ -113,20 +112,16 @@ class CommentItem(
|
||||
|
||||
viewBinding.commentUserName.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
commentsFragment.activity,
|
||||
Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
.putExtra("userId", comment.userId.toInt())
|
||||
.putExtra("userLVL", "[${levelColor.second}]"),
|
||||
null
|
||||
.putExtra("userLVL","[${levelColor.second}]"), null
|
||||
)
|
||||
}
|
||||
viewBinding.commentUserAvatar.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
commentsFragment.activity,
|
||||
Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
|
||||
.putExtra("userId", comment.userId.toInt())
|
||||
.putExtra("userLVL", "[${levelColor.second}]"),
|
||||
null
|
||||
.putExtra("userLVL","[${levelColor.second}]"), null
|
||||
)
|
||||
}
|
||||
viewBinding.commentText.setOnLongClickListener {
|
||||
@@ -148,10 +143,8 @@ class CommentItem(
|
||||
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.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) {
|
||||
@@ -280,16 +273,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -297,7 +286,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()
|
||||
@@ -317,13 +306,11 @@ 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)
|
||||
@@ -368,8 +355,7 @@ 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) {
|
||||
@@ -387,17 +373,16 @@ 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,10 +75,7 @@ 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
|
||||
@@ -304,7 +301,7 @@ class CommentsFragment : Fragment() {
|
||||
|
||||
activity.binding.commentLabel.setOnClickListener {
|
||||
//alert dialog to enter a number, with a cancel and ok button
|
||||
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
.setTitle("Enter a chapter/episode number tag")
|
||||
.setView(R.layout.dialog_edittext)
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
@@ -580,7 +577,7 @@ class CommentsFragment : Fragment() {
|
||||
* Called when the user tries to comment for the first time
|
||||
*/
|
||||
private fun showCommentRulesDialog() {
|
||||
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
.setTitle("Commenting Rules")
|
||||
.setMessage(
|
||||
"I WILL BAN YOU WITHOUT HESITATION\n" +
|
||||
|
||||
@@ -343,7 +343,6 @@ class MangaChapterAdapter(
|
||||
fun updateType(t: Int) {
|
||||
type = t
|
||||
}
|
||||
|
||||
private fun formatDate(timestamp: Long?): String {
|
||||
timestamp ?: return "" // Return empty string if timestamp is null
|
||||
|
||||
@@ -367,7 +366,6 @@ class MangaChapterAdapter(
|
||||
else -> "Just now"
|
||||
}
|
||||
}
|
||||
|
||||
1L -> "1 day ago"
|
||||
in 2..6 -> "$daysDifference days ago"
|
||||
else -> SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(targetDate)
|
||||
|
||||
@@ -229,7 +229,7 @@ class MangaReadAdapter(
|
||||
refresh = true
|
||||
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
startActivity(fragment.requireContext(), intent, null)
|
||||
ContextCompat.startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,10 +258,8 @@ 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
|
||||
@@ -443,11 +441,7 @@ 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 ->
|
||||
@@ -476,11 +470,7 @@ 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)
|
||||
}
|
||||
@@ -501,14 +491,11 @@ 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)
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -34,16 +35,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.compareName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||
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.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
@@ -194,7 +194,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
)
|
||||
|
||||
for (download in downloadManager.mangaDownloadedTypes) {
|
||||
if (media.compareName(download.title)) {
|
||||
if (download.title == media.mainName().findValidName()) {
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
}
|
||||
@@ -203,10 +203,6 @@ 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
|
||||
@@ -495,7 +491,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
if (success) {
|
||||
continueDownload()
|
||||
} else {
|
||||
snackString(getString(R.string.download_permission_required))
|
||||
snackString("Permission is required to download")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -42,8 +42,7 @@ 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
|
||||
|
||||
@@ -56,8 +56,8 @@ import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaSingleton
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
@@ -129,12 +129,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
var sliding = false
|
||||
var isAnimating = false
|
||||
|
||||
private val directionRLBT
|
||||
get() = defaultSettings.direction == RIGHT_TO_LEFT
|
||||
|| defaultSettings.direction == BOTTOM_TO_TOP
|
||||
private val directionPagedBT
|
||||
get() = defaultSettings.layout == CurrentReaderSettings.Layouts.PAGED
|
||||
&& defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP
|
||||
private val directionRLBT get() = defaultSettings.direction == RIGHT_TO_LEFT
|
||||
|| defaultSettings.direction == BOTTOM_TO_TOP
|
||||
private val directionPagedBT get() = defaultSettings.layout == CurrentReaderSettings.Layouts.PAGED
|
||||
&& defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
|
||||
@@ -231,7 +229,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
|
||||
?: 1))
|
||||
else
|
||||
if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) {
|
||||
if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP ) {
|
||||
binding.mangaReaderPager.currentItem =
|
||||
(maxChapterPage.toInt() - value.toInt()) / (dualPage { 2 } ?: 1)
|
||||
} else {
|
||||
@@ -347,11 +345,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
|
||||
else snackString(getString(R.string.first_chapter))
|
||||
} else {
|
||||
if (chaptersArr.size > currentChapterIndex + 1) progress {
|
||||
change(
|
||||
currentChapterIndex + 1
|
||||
)
|
||||
}
|
||||
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
|
||||
else snackString(getString(R.string.next_chapter_not_found))
|
||||
}
|
||||
}
|
||||
@@ -361,11 +355,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
binding.mangaReaderPreviousChapter.setOnClickListener {
|
||||
if (directionRLBT) {
|
||||
if (chaptersArr.size > currentChapterIndex + 1) progress {
|
||||
change(
|
||||
currentChapterIndex + 1
|
||||
)
|
||||
}
|
||||
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
|
||||
else snackString(getString(R.string.next_chapter_not_found))
|
||||
} else {
|
||||
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
|
||||
@@ -382,15 +372,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
currentChapterIndex = chaptersArr.indexOf(chap.number)
|
||||
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
||||
if (directionRLBT) {
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
} else {
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
}
|
||||
applySettings()
|
||||
val context = this
|
||||
@@ -403,12 +389,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
"nothing" -> mutableListOf(
|
||||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||
)
|
||||
|
||||
"dantotsu" -> mutableListOf(
|
||||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||
RPC.Link("Read on Dantotsu", getString(R.string.dantotsu))
|
||||
)
|
||||
|
||||
"anilist" -> {
|
||||
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
|
||||
val anilistLink = "https://anilist.co/user/$userId/"
|
||||
@@ -417,7 +401,6 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
RPC.Link("View My AniList", anilistLink)
|
||||
)
|
||||
}
|
||||
|
||||
else -> mutableListOf()
|
||||
}
|
||||
val presence = RPC.createPresence(
|
||||
@@ -428,12 +411,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
details = chap.title?.takeIf { it.isNotEmpty() }
|
||||
?: getString(R.string.chapter_num, chap.number),
|
||||
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
|
||||
largeImage = media.cover?.let { cover ->
|
||||
RPC.Link(
|
||||
media.userPreferredName,
|
||||
cover
|
||||
)
|
||||
},
|
||||
largeImage = media.cover?.let { cover -> RPC.Link(media.userPreferredName, cover) },
|
||||
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
|
||||
buttons = buttons
|
||||
)
|
||||
)
|
||||
@@ -940,12 +919,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
isAnimating = true
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f)
|
||||
.setDuration(controllerDuration).start()
|
||||
ObjectAnimator.ofFloat(
|
||||
binding.mangaReaderBottomLayout,
|
||||
"translationY",
|
||||
0f,
|
||||
128f
|
||||
)
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Parcelable
|
||||
@@ -12,13 +13,13 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
@@ -26,22 +27,19 @@ import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.novel.NovelDownloaderService
|
||||
import ani.dantotsu.download.novel.NovelServiceDataSingleton
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.StoragePermissions
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class NovelReadFragment : Fragment(),
|
||||
DownloadTriggerCallback,
|
||||
@@ -64,40 +62,26 @@ class NovelReadFragment : Fragment(),
|
||||
|
||||
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
|
||||
Logger.log("novel link: ${novelDownloadPackage.link}")
|
||||
activity?.let {
|
||||
fun continueDownload() {
|
||||
val downloadTask = NovelDownloaderService.DownloadTask(
|
||||
title = media.mainName(),
|
||||
chapter = novelDownloadPackage.novelName,
|
||||
downloadLink = novelDownloadPackage.link,
|
||||
originalLink = novelDownloadPackage.originalLink,
|
||||
sourceMedia = media,
|
||||
coverUrl = novelDownloadPackage.coverUrl,
|
||||
retries = 2,
|
||||
)
|
||||
NovelServiceDataSingleton.downloadQueue.offer(downloadTask)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val downloadTask = NovelDownloaderService.DownloadTask(
|
||||
title = media.mainName(),
|
||||
chapter = novelDownloadPackage.novelName,
|
||||
downloadLink = novelDownloadPackage.link,
|
||||
originalLink = novelDownloadPackage.originalLink,
|
||||
sourceMedia = media,
|
||||
coverUrl = novelDownloadPackage.coverUrl,
|
||||
retries = 2,
|
||||
)
|
||||
NovelServiceDataSingleton.downloadQueue.offer(downloadTask)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
if (!NovelServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, NovelDownloaderService::class.java)
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(requireContext(), intent)
|
||||
}
|
||||
NovelServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
if (!NovelServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, NovelDownloaderService::class.java)
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(requireContext(), intent)
|
||||
}
|
||||
NovelServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
if (!StoragePermissions.hasDirAccess(it)) {
|
||||
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
|
||||
if (success) {
|
||||
continueDownload()
|
||||
} else {
|
||||
snackString(getString(R.string.download_permission_required))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continueDownload()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,12 +97,7 @@ class NovelReadFragment : Fragment(),
|
||||
) {
|
||||
try {
|
||||
val directory =
|
||||
DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
novel.name
|
||||
)
|
||||
DownloadsManager.getSubDirectory(context?:currContext()!!, MediaType.NOVEL, false, novel.name)
|
||||
val file = directory?.findFile(novel.name)
|
||||
if (file?.exists() == false) return false
|
||||
val fileUri = file?.uri ?: return false
|
||||
|
||||
@@ -181,7 +181,7 @@ class NovelResponseAdapter(
|
||||
if (position != -1) {
|
||||
list[position].extra?.remove("0")
|
||||
list[position].extra?.set("0", "Downloading: $progress%")
|
||||
Logger.log("updateDownloadProgress: $progress, position: $position")
|
||||
Logger.log( "updateDownloadProgress: $progress, position: $position")
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,11 +292,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
applySettings()
|
||||
}
|
||||
|
||||
val cfi = PrefManager.getNullableCustomVal(
|
||||
"${sanitizedBookId}_progress",
|
||||
null,
|
||||
String::class.java
|
||||
)
|
||||
val cfi = PrefManager.getNullableCustomVal("${sanitizedBookId}_progress", null, String::class.java)
|
||||
|
||||
cfi?.let { binding.bookReader.goto(it) }
|
||||
binding.progress.visibility = View.GONE
|
||||
|
||||
@@ -70,10 +70,8 @@ class ListActivity : AppCompatActivity() {
|
||||
setContentView(binding.root)
|
||||
|
||||
val anime = intent.getBooleanExtra("anime", true)
|
||||
binding.listTitle.text = getString(
|
||||
R.string.user_list, intent.getStringExtra("username"),
|
||||
if (anime) getString(R.string.anime) else getString(R.string.manga)
|
||||
)
|
||||
binding.listTitle.text = getString(R.string.user_list, intent.getStringExtra("username"),
|
||||
if (anime) getString(R.string.anime) else getString(R.string.manga))
|
||||
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
this@ListActivity.selectedTabIdx = tab?.position ?: 0
|
||||
@@ -160,8 +158,7 @@ class ListActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
binding.filter.setOnClickListener {
|
||||
val genres =
|
||||
PrefManager.getVal<Set<String>>(PrefName.GenresList).toMutableSet().sorted()
|
||||
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList).toMutableSet().sorted()
|
||||
val popup = PopupMenu(this, it)
|
||||
popup.menu.add("All")
|
||||
genres.forEach { genre ->
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import ani.dantotsu.notifications.TaskScheduler.TaskType
|
||||
import ani.dantotsu.notifications.anilist.AnilistNotificationReceiver
|
||||
import ani.dantotsu.notifications.comment.CommentNotificationReceiver
|
||||
import ani.dantotsu.notifications.TaskScheduler.TaskType
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionNotificationReceiver
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import ani.dantotsu.notifications.TaskScheduler.TaskType
|
||||
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
||||
import ani.dantotsu.notifications.comment.CommentNotificationWorker
|
||||
import ani.dantotsu.notifications.TaskScheduler.TaskType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.util.Logger
|
||||
|
||||
@@ -21,16 +21,11 @@ interface TaskScheduler {
|
||||
for (taskType in TaskType.entries) {
|
||||
val interval = when (taskType) {
|
||||
TaskType.COMMENT_NOTIFICATION -> CommentNotificationWorker.checkIntervals[PrefManager.getVal(
|
||||
PrefName.CommentNotificationInterval
|
||||
)]
|
||||
|
||||
PrefName.CommentNotificationInterval)]
|
||||
TaskType.ANILIST_NOTIFICATION -> AnilistNotificationWorker.checkIntervals[PrefManager.getVal(
|
||||
PrefName.AnilistNotificationInterval
|
||||
)]
|
||||
|
||||
PrefName.AnilistNotificationInterval)]
|
||||
TaskType.SUBSCRIPTION_NOTIFICATION -> SubscriptionNotificationWorker.checkIntervals[PrefManager.getVal(
|
||||
PrefName.SubscriptionNotificationInterval
|
||||
)]
|
||||
PrefName.SubscriptionNotificationInterval)]
|
||||
}
|
||||
scheduleRepeatingTask(taskType, interval)
|
||||
}
|
||||
@@ -67,7 +62,6 @@ interface TaskScheduler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class TaskType {
|
||||
COMMENT_NOTIFICATION,
|
||||
ANILIST_NOTIFICATION,
|
||||
|
||||
@@ -66,7 +66,6 @@ class MediaNameFetch {
|
||||
val type = object : TypeToken<MediaResponse>() {}.type
|
||||
return gson.fromJson(response, type)
|
||||
}
|
||||
|
||||
data class ReturnedData(val title: String, val coverImage: String, val color: String)
|
||||
|
||||
data class MediaResponse(val data: Map<String, MediaItem>)
|
||||
|
||||
@@ -32,7 +32,7 @@ class SubscriptionHelper {
|
||||
return data
|
||||
}
|
||||
|
||||
private fun saveSelected(mediaId: Int, data: Selected) {
|
||||
private fun saveSelected( mediaId: Int, data: Selected) {
|
||||
PrefManager.setCustomVal("${mediaId}-select", data)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@ import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class AndroidBug5497Workaround private constructor(
|
||||
activity: Activity,
|
||||
private val callback: (Boolean) -> Unit
|
||||
) {
|
||||
class AndroidBug5497Workaround private constructor(activity: Activity, private val callback: (Boolean) -> Unit) {
|
||||
private val mChildOfContent: View
|
||||
private var usableHeightPrevious = 0
|
||||
private val frameLayoutParams: FrameLayout.LayoutParams
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ani.dantotsu.others
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -7,6 +8,7 @@ import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
@@ -17,6 +19,9 @@ import ani.dantotsu.parsers.Book
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
object Download {
|
||||
|
||||
@@ -2,10 +2,10 @@ package ani.dantotsu.others
|
||||
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.gson.Gson
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import kotlinx.serialization.SerialName
|
||||
|
||||
@@ -56,10 +56,9 @@ class OutlineTextView : AppCompatTextView {
|
||||
setStrokeWidth(strokeWidth)
|
||||
}
|
||||
|
||||
private val Float.toPx
|
||||
get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
|
||||
)
|
||||
private val Float.toPx get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
|
||||
)
|
||||
|
||||
private fun setStrokeWidth(width: Float) {
|
||||
strokeWidth = width.toPx
|
||||
|
||||
@@ -12,8 +12,8 @@ import ani.dantotsu.R
|
||||
class Xpandable @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
private var expanded: Boolean = false
|
||||
private var listeners: ArrayList<OnChangeListener> = arrayListOf()
|
||||
var expanded: Boolean = false
|
||||
private var listener: OnChangeListener? = null
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.Xpandable) {
|
||||
@@ -50,9 +50,7 @@ class Xpandable @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
postDelayed({
|
||||
listeners.forEach {
|
||||
it.onRetract()
|
||||
}
|
||||
listener?.onRetract()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@@ -66,19 +64,13 @@ class Xpandable @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
postDelayed({
|
||||
listeners.forEach {
|
||||
it.onExpand()
|
||||
}
|
||||
listener?.onExpand()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun addOnChangeListener(listener: OnChangeListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: OnChangeListener) {
|
||||
listeners.remove(listener)
|
||||
fun setOnChangeListener(listener: OnChangeListener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
interface OnChangeListener {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user