diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml
index ba36e72be61..14e28c5e6a4 100644
--- a/.github/workflows/beta.yml
+++ b/.github/workflows/beta.yml
@@ -3,12 +3,15 @@ name: Build APK and Notify Discord
on:
push:
branches:
- - main
- dev
+ paths-ignore:
+ - '**/README.md'
jobs:
build:
runs-on: ubuntu-latest
+ env:
+ CI: true
steps:
- name: Checkout repo
@@ -29,11 +32,17 @@ jobs:
java-version: 17
cache: gradle
+ - name: Decode Keystore File
+ run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
+
+ - name: List files in the directory
+ run: ls -l
+
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
- run: ./gradlew assembleDebug
+ run: ./gradlew assembleDebug -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@v3.0.0
@@ -45,7 +54,7 @@ jobs:
shell: bash
run: |
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" )
- curl -F "payload_json={\"content\":\" everyone **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
+ curl -F "payload_json={\"content\":\" Debug-Build **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
- name: Delete Old Pre-Releases
id: delete-pre-releases
diff --git a/README.md b/README.md
index a5605a6c5cc..adc78f62296 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
-
+
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
@@ -31,6 +31,10 @@ You can come hang out with our awesome community, request new features, and repo
+## VISITORS
+
+
+
## LICENSE 📜
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
diff --git a/app/build.gradle b/app/build.gradle
index 1cde5bdba29..df3a7889326 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,7 +21,7 @@ android {
minSdk 23
targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger())
- versionName "2.0.0-beta00-i"
+ versionName "2.0.0-beta01-iv1"
signingConfig signingConfigs.debug
}
@@ -29,12 +29,12 @@ android {
debug {
applicationIdSuffix ".beta"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
- debuggable true
+ debuggable System.getenv("CI") == null
}
release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
debuggable false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
}
}
buildFeatures {
@@ -54,19 +54,19 @@ android {
dependencies {
// Core
implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'androidx.browser:browser:1.6.0'
+ implementation 'androidx.browser:browser:1.7.0'
implementation 'androidx.core:core-ktx:1.12.0'
- implementation 'androidx.fragment:fragment-ktx:1.6.1'
+ implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
- implementation "androidx.work:work-runtime-ktx:2.8.1"
+ implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'com.google.code.gson:gson:2.8.9'
+ implementation 'com.google.code.gson:gson:2.10'
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
- implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
- implementation 'androidx.preference:preference:1.2.1'
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.9.0'
// Glide
@@ -90,9 +90,12 @@ dependencies {
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
implementation "androidx.media3:media3-session:$exo_version"
+ //media3 casting
+ implementation "androidx.media3:media3-cast:$exo_version"
+ implementation "androidx.mediarouter:mediarouter:1.6.0"
// UI
- implementation 'com.google.android.material:material:1.10.0'
+ implementation 'com.google.android.material:material:1.11.0'
implementation 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'com.flaviofaria:kenburnsview:1.0.7'
@@ -100,7 +103,7 @@ dependencies {
implementation 'com.alexvasilkov:gesture-views:2.8.3'
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
- implementation "com.github.skydoves:colorpickerview:2.3.0"
+ implementation 'com.github.eltos:simpledialogfragments:v3.7'
// string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
@@ -115,13 +118,14 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
- implementation 'com.squareup.okio:okio:3.3.0'
- implementation 'ch.acra:acra-http:5.9.7'
+ implementation 'com.squareup.okio:okio:3.7.0'
+ implementation 'ch.acra:acra-http:5.11.3'
implementation 'org.jsoup:jsoup:1.15.4'
- implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.5.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
- implementation 'androidx.preference:preference-ktx:1.2.0'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
+ implementation 'app.cash.quickjs:quickjs-android:0.9.2'
}
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
index ef58c36a6eb..92f0ffec672 100644
--- a/app/src/debug/res/values/strings.xml
+++ b/app/src/debug/res/values/strings.xml
@@ -1,4 +1,4 @@
- Dantotsu ß
+ Dantotsu β
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a77c0d318bb..c61e37dd073 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,8 @@
android:required="false" />
+
@@ -17,25 +19,22 @@
-
-
-
+
-
-
-
+
-
-
-
+
-
-
@@ -48,6 +47,7 @@
+ tools:ignore="AllowBackup">
+
+
+
+
+
+
+
+
+
+
+ android:exported="true">
+
@@ -71,13 +85,11 @@
-
-
@@ -103,9 +115,9 @@
-
+
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
-
+ android:exported="false"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+ android:exported="false"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar" />
-
+
@@ -258,32 +287,49 @@
android:resource="@xml/provider_paths" />
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/App.kt b/app/src/main/java/ani/dantotsu/App.kt
index 89a5d4415c1..5b6869ea99c 100644
--- a/app/src/main/java/ani/dantotsu/App.kt
+++ b/app/src/main/java/ani/dantotsu/App.kt
@@ -13,7 +13,9 @@ import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.novel.NovelExtensionManager
+import ani.dantotsu.settings.SettingsActivity
import com.google.android.material.color.DynamicColors
+import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import eu.kanade.tachiyomi.data.notification.Notifications
@@ -58,6 +60,24 @@ class App : MultiDexApplication() {
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
+ getSharedPreferences(
+ getString(R.string.preference_file_key),
+ Context.MODE_PRIVATE
+ ).getBoolean("shared_user_id", true).let {
+ if (!it) return@let
+ val dUsername = getSharedPreferences(
+ getString(R.string.preference_file_key),
+ Context.MODE_PRIVATE
+ ).getString("discord_username", null)
+ val aUsername = getSharedPreferences(
+ getString(R.string.preference_file_key),
+ Context.MODE_PRIVATE
+ ).getString("anilist_username", null)
+ if (dUsername != null || aUsername != null) {
+ Firebase.crashlytics.setUserId("$dUsername - $aUsername")
+ }
+ }
+ FirebaseCrashlytics.getInstance().setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
@@ -77,13 +97,13 @@ class App : MultiDexApplication() {
animeScope.launch {
animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
- AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
+ AnimeSources.init(animeExtensionManager.installedExtensionsFlow, this@App)
}
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
- MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
+ MangaSources.init(mangaExtensionManager.installedExtensionsFlow, this@App)
}
val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch {
@@ -91,7 +111,6 @@ class App : MultiDexApplication() {
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
}
-
}
diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt
index 6a789743f16..da5b5e1fbe9 100644
--- a/app/src/main/java/ani/dantotsu/Functions.kt
+++ b/app/src/main/java/ani/dantotsu/Functions.kt
@@ -4,6 +4,8 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.DatePickerDialog
+import android.app.NotificationManager
+import android.app.PendingIntent
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@@ -28,6 +30,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.*
import android.widget.*
import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp
@@ -44,6 +47,7 @@ import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.settings.UserInterfaceSettings
+import ani.dantotsu.subcriptions.NotificationClickReceiver
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
@@ -53,6 +57,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.*
import nl.joery.animatedbottombar.AnimatedBottomBar
import java.io.*
@@ -124,6 +130,13 @@ fun loadData(fileName: String, context: Context? = null, toast: Boolean = tr
}
} catch (e: Exception) {
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
+ //try to delete the file
+ try {
+ a?.deleteFile(fileName)
+ } catch (e: Exception) {
+ FirebaseCrashlytics.getInstance().log("Failed to delete file $fileName")
+ FirebaseCrashlytics.getInstance().recordException(e)
+ }
e.printStackTrace()
}
return null
@@ -601,7 +614,7 @@ fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
"$APPLICATION_ID.provider",
saveImage(
bitmap,
- Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS,
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title
) ?: return
)
@@ -624,13 +637,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png")
- return tryWith {
+ return try {
val fOut: OutputStream = FileOutputStream(imageFile)
image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
fOut.close()
scanFile(imageFile.absolutePath, currContext()!!)
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
imageFile
+ } catch (e: Exception) {
+ snackString("Failed to save image: ${e.localizedMessage}")
+ null
}
}
@@ -673,7 +689,7 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
@SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) {
- if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) {
+ if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
@@ -783,35 +799,40 @@ fun toast(string: String?) {
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
- if (s != null) {
- (activity ?: currActivity())?.apply {
- runOnUiThread {
- val snackBar = Snackbar.make(
- window.decorView.findViewById(android.R.id.content),
- s,
- Snackbar.LENGTH_SHORT
- )
- snackBar.view.apply {
- updateLayoutParams {
- gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
- width = WRAP_CONTENT
- }
- translationY = -(navBarHeight.dp + 32f)
- translationZ = 32f
- updatePadding(16f.px, right = 16f.px)
- setOnClickListener {
- snackBar.dismiss()
- }
- setOnLongClickListener {
- copyToClipboard(clipboard ?: s, false)
- toast(getString(R.string.copied_to_clipboard))
- true
+ try { //I have no idea why this sometimes crashes for some people...
+ if (s != null) {
+ (activity ?: currActivity())?.apply {
+ runOnUiThread {
+ val snackBar = Snackbar.make(
+ window.decorView.findViewById(android.R.id.content),
+ s,
+ Snackbar.LENGTH_SHORT
+ )
+ snackBar.view.apply {
+ updateLayoutParams {
+ gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
+ width = WRAP_CONTENT
+ }
+ translationY = -(navBarHeight.dp + 32f)
+ translationZ = 32f
+ updatePadding(16f.px, right = 16f.px)
+ setOnClickListener {
+ snackBar.dismiss()
+ }
+ setOnLongClickListener {
+ copyToClipboard(clipboard ?: s, false)
+ toast(getString(R.string.copied_to_clipboard))
+ true
+ }
}
+ snackBar.show()
}
- snackBar.show()
}
+ logger(s)
}
- logger(s)
+ } catch (e: Exception) {
+ logger(e.stackTraceToString())
+ FirebaseCrashlytics.getInstance().recordException(e)
}
}
@@ -930,6 +951,33 @@ fun checkCountry(context: Context): Boolean {
}
}
+const val INCOGNITO_CHANNEL_ID = 26
+
+@SuppressLint("LaunchActivityFromNotification")
+fun incognitoNotification(context: Context) {
+ val notificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val incognito = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ .getBoolean("incognito", false)
+ if (incognito) {
+ val intent = Intent(context, NotificationClickReceiver::class.java)
+ val pendingIntent = PendingIntent.getBroadcast(
+ context, 0, intent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ val builder = NotificationCompat.Builder(context, Notifications.CHANNEL_INCOGNITO_MODE)
+ .setSmallIcon(R.drawable.ic_incognito_24)
+ .setContentTitle("Incognito Mode")
+ .setContentText("Disable Incognito Mode")
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ notificationManager.notify(INCOGNITO_CHANNEL_ID, builder.build())
+ } else {
+ notificationManager.cancel(INCOGNITO_CHANNEL_ID)
+ }
+}
+
suspend fun View.pop() {
currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt
index b27ee17dada..2be1a87d04d 100644
--- a/app/src/main/java/ani/dantotsu/MainActivity.kt
+++ b/app/src/main/java/ani/dantotsu/MainActivity.kt
@@ -1,6 +1,7 @@
package ani.dantotsu
import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Animatable
@@ -11,12 +12,14 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
+import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.viewModels
+import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
@@ -26,11 +29,14 @@ import androidx.fragment.app.Fragment
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 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
@@ -39,12 +45,14 @@ import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.LangSet
+import ani.dantotsu.others.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.themes.ThemeManager
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -54,17 +62,23 @@ import java.io.Serializable
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
+ private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
private val scope = lifecycleScope
private var load = false
private var uiSettings = UserInterfaceSettings()
+ @SuppressLint("InternalInsetResource", "DiscouragedApi")
+ @OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
super.onCreate(savedInstanceState)
+ //get FRAGMENT_CLASS_NAME from intent
+ val FRAGMENT_CLASS_NAME = intent.getStringExtra("FRAGMENT_CLASS_NAME")
+
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -73,17 +87,59 @@ class MainActivity : AppCompatActivity() {
val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
- val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
+ val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable
}
- val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
- .getBoolean("colorOverflow", false)
+ val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ val colorOverflow = sharedPreferences.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
}
+ val offset = try {
+ val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
+ resources.getDimensionPixelSize(statusBarHeightId)
+ } catch (e: Exception) {
+ statusBarHeight
+ }
+ val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
+ layoutParams.topMargin = 11 * offset / 12
+ binding.incognito.layoutParams = layoutParams
+ incognitoLiveData = SharedPreferenceBooleanLiveData(
+ sharedPreferences,
+ "incognito",
+ false
+ )
+ incognitoLiveData.observe(this) {
+ if (it) {
+ val slideDownAnim = ObjectAnimator.ofFloat(
+ binding.incognito,
+ View.TRANSLATION_Y,
+ -(binding.incognito.height.toFloat() + statusBarHeight),
+ 0f
+ )
+ slideDownAnim.duration = 200
+ slideDownAnim.start()
+ binding.incognito.visibility = View.VISIBLE
+ } else {
+ val slideUpAnim = ObjectAnimator.ofFloat(
+ binding.incognito,
+ View.TRANSLATION_Y,
+ 0f,
+ -(binding.incognito.height.toFloat() + statusBarHeight)
+ )
+ slideUpAnim.duration = 200
+ slideUpAnim.start()
+ //wait for animation to finish
+ Handler(Looper.getMainLooper()).postDelayed(
+ { binding.incognito.visibility = View.GONE },
+ 200
+ )
+ }
+ }
+ incognitoNotification(this)
var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) {
@@ -142,106 +198,146 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
- selectedOption = uiSettings.defaultStartUpTab
+ selectedOption = if (FRAGMENT_CLASS_NAME != null) {
+ when (FRAGMENT_CLASS_NAME) {
+ AnimeFragment::class.java.name -> 0
+ HomeFragment::class.java.name -> 1
+ MangaFragment::class.java.name -> 2
+ else -> 1
+ }
+ } else {
+ uiSettings.defaultStartUpTab
+ }
binding.includedNavbar.navbarContainer.updateLayoutParams {
bottomMargin = navBarHeight
+
}
}
-
+ val offline = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ .getBoolean("offlineMode", false)
if (!isOnline(this)) {
snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java))
} else {
- val model: AnilistHomeViewModel by viewModels()
- model.genres.observe(this) {
- if (it != null) {
- if (it) {
- val navbar = binding.includedNavbar.navbar
- bottomBar = navbar
- navbar.visibility = View.VISIBLE
- binding.mainProgressBar.visibility = View.GONE
- val mainViewPager = binding.viewpager
- mainViewPager.isUserInputEnabled = false
- mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
- mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
- navbar.setOnTabSelectListener(object :
- AnimatedBottomBar.OnTabSelectListener {
- override fun onTabSelected(
- lastIndex: Int,
- lastTab: AnimatedBottomBar.Tab?,
- newIndex: Int,
- newTab: AnimatedBottomBar.Tab
- ) {
- navbar.animate().translationZ(12f).setDuration(200).start()
- selectedOption = newIndex
- mainViewPager.setCurrentItem(newIndex, false)
+ if (offline) {
+ snackString(this@MainActivity.getString(R.string.no_internet_connection))
+ startActivity(Intent(this, NoInternet::class.java))
+ } else {
+ val model: AnilistHomeViewModel by viewModels()
+ model.genres.observe(this) { it ->
+ if (it != null) {
+ if (it) {
+ val navbar = binding.includedNavbar.navbar
+ bottomBar = navbar
+ navbar.visibility = View.VISIBLE
+ binding.mainProgressBar.visibility = View.GONE
+ val mainViewPager = binding.viewpager
+ mainViewPager.isUserInputEnabled = false
+ mainViewPager.adapter =
+ ViewPagerAdapter(supportFragmentManager, lifecycle)
+ mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
+ navbar.setOnTabSelectListener(object :
+ AnimatedBottomBar.OnTabSelectListener {
+ override fun onTabSelected(
+ lastIndex: Int,
+ lastTab: AnimatedBottomBar.Tab?,
+ newIndex: Int,
+ newTab: AnimatedBottomBar.Tab
+ ) {
+ navbar.animate().translationZ(12f).setDuration(200).start()
+ selectedOption = newIndex
+ mainViewPager.setCurrentItem(newIndex, false)
+ }
+ })
+ navbar.selectTabAt(selectedOption)
+ mainViewPager.post {
+ mainViewPager.setCurrentItem(
+ selectedOption,
+ false
+ )
}
- })
- navbar.selectTabAt(selectedOption)
- mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
- } else {
- binding.mainProgressBar.visibility = View.GONE
+ } else {
+ binding.mainProgressBar.visibility = View.GONE
+ }
}
}
- }
- //Load Data
- if (!load) {
- scope.launch(Dispatchers.IO) {
- model.loadMain(this@MainActivity)
- val id = intent.extras?.getInt("mediaId", 0)
- val isMAL = intent.extras?.getBoolean("mal") ?: false
- val cont = intent.extras?.getBoolean("continue") ?: false
- if (id != null && id != 0) {
- val media = withContext(Dispatchers.IO) {
- Anilist.query.getMedia(id, isMAL)
- }
- if (media != null) {
- media.cameFromContinue = cont
- startActivity(
- Intent(this@MainActivity, MediaDetailsActivity::class.java)
- .putExtra("media", media as Serializable)
- )
- } else {
- snackString(this@MainActivity.getString(R.string.anilist_not_found))
+ //Load Data
+ if (!load) {
+ scope.launch(Dispatchers.IO) {
+ model.loadMain(this@MainActivity)
+ val id = intent.extras?.getInt("mediaId", 0)
+ val isMAL = intent.extras?.getBoolean("mal") ?: false
+ val cont = intent.extras?.getBoolean("continue") ?: false
+ if (id != null && id != 0) {
+ val media = withContext(Dispatchers.IO) {
+ Anilist.query.getMedia(id, isMAL)
+ }
+ if (media != null) {
+ media.cameFromContinue = cont
+ startActivity(
+ Intent(this@MainActivity, MediaDetailsActivity::class.java)
+ .putExtra("media", media as Serializable)
+ )
+ } else {
+ snackString(this@MainActivity.getString(R.string.anilist_not_found))
+ }
}
+ delay(500)
+ startSubscription()
}
- delay(500)
- startSubscription()
+ load = true
}
- load = true
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- if (loadData("allow_opening_links", this) != true) {
- CustomBottomDialog.newInstance().apply {
- title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
- val md = "Open settings & click +Add Links & select Anilist & Mal urls"
- addView(TextView(this@MainActivity).apply {
- val markWon =
- Markwon.builder(this@MainActivity)
- .usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
- markWon.setMarkdown(this, md)
- })
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (loadData("allow_opening_links", this) != true) {
+ CustomBottomDialog.newInstance().apply {
+ title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
+ val md = "Open settings & click +Add Links & select Anilist & Mal urls"
+ addView(TextView(this@MainActivity).apply {
+ val markWon =
+ Markwon.builder(this@MainActivity)
+ .usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
+ markWon.setMarkdown(this, md)
+ })
- setNegativeButton(this@MainActivity.getString(R.string.no)) {
- saveData("allow_opening_links", true, this@MainActivity)
- dismiss()
- }
+ setNegativeButton(this@MainActivity.getString(R.string.no)) {
+ saveData("allow_opening_links", true, this@MainActivity)
+ dismiss()
+ }
- setPositiveButton(this@MainActivity.getString(R.string.yes)) {
- saveData("allow_opening_links", true, this@MainActivity)
- tryWith(true) {
- startActivity(
- Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
- .setData(Uri.parse("package:$packageName"))
- )
+ setPositiveButton(this@MainActivity.getString(R.string.yes)) {
+ saveData("allow_opening_links", true, this@MainActivity)
+ tryWith(true) {
+ startActivity(
+ Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
+ .setData(Uri.parse("package:$packageName"))
+ )
+ }
}
- }
- }.show(supportFragmentManager, "dialog")
+ }.show(supportFragmentManager, "dialog")
+ }
}
}
}
+ //TODO: Remove this
+ GlobalScope.launch(Dispatchers.IO) {
+ val index = Helper.downloadManager(this@MainActivity).downloadIndex
+ val downloadCursor = index.getDownloads()
+ while (downloadCursor.moveToNext()) {
+ val download = downloadCursor.download
+ Log.e("Downloader", download.request.uri.toString())
+ Log.e("Downloader", download.request.id.toString())
+ Log.e("Downloader", download.request.mimeType.toString())
+ Log.e("Downloader", download.request.data.size.toString())
+ Log.e("Downloader", download.bytesDownloaded.toString())
+ Log.e("Downloader", download.state.toString())
+ Log.e("Downloader", download.failureReason.toString())
+ if (download.state == Download.STATE_FAILED) { //simple cleanup
+ Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
+ }
+ }
+ }
}
@@ -261,4 +357,4 @@ class MainActivity : AppCompatActivity() {
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt
index 38b9fc4cb41..19e45a589f4 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt
+++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt
@@ -3,7 +3,10 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application
import android.content.Context
+import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.parsers.novel.NovelExtensionManager
@@ -27,6 +30,7 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
+ @OptIn(UnstableApi::class)
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
@@ -51,6 +55,8 @@ class AppModule(val app: Application) : InjektModule {
}
}
+ addSingletonFactory { StandaloneDatabaseProvider(app) }
+
addSingletonFactory { MangaCache() }
ContextCompat.getMainExecutor(app).execute {
diff --git a/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt b/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt
index f7084645265..fb08c59d7e5 100644
--- a/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt
+++ b/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt
@@ -10,7 +10,6 @@ import ani.dantotsu.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
fun updateProgress(media: Media, number: String) {
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
index a0c1c270f09..5c344b59372 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
@@ -1,6 +1,7 @@
package ani.dantotsu.connections.anilist
import android.app.Activity
+import android.content.Context
import ani.dantotsu.R
import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId
@@ -10,6 +11,7 @@ import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Page
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.currContext
+import ani.dantotsu.isOnline
import ani.dantotsu.loadData
import ani.dantotsu.logError
import ani.dantotsu.media.Author
@@ -33,6 +35,13 @@ class AnilistQueries {
}.also { println("time : $it") }
val user = response?.data?.user ?: return false
+ currContext()?.let {
+ it.getSharedPreferences(it.getString(R.string.preference_file_key), Context.MODE_PRIVATE)
+ .edit()
+ .putString("anilist_username", user.name)
+ .apply()
+ }
+
Anilist.userid = user.id
Anilist.username = user.name
Anilist.bg = user.bannerImage
@@ -239,7 +248,9 @@ class AnilistQueries {
else snackString(currContext()?.getString(R.string.what_did_you_open))
}
} else {
- snackString(currContext()?.getString(R.string.error_getting_data))
+ if (currContext()?.let { isOnline(it) } == true) {
+ snackString(currContext()?.getString(R.string.error_getting_data))
+ }
}
}
val mal = async {
@@ -410,8 +421,9 @@ class AnilistQueries {
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
sorted["All"] = all
-
- val sort = sortOrder ?: options?.rowOrder
+ val listsort = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getString("sort_order", "score")
+ val sort = listsort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) {
when (sort) {
"score" -> sorted[i]?.sortWith { b, a ->
diff --git a/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt b/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt
deleted file mode 100644
index 21f0a9100ce..00000000000
--- a/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package ani.dantotsu.download
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.Fragment
-import ani.dantotsu.R
-import ani.dantotsu.others.LangSet
-import ani.dantotsu.themes.ThemeManager
-
-class DownloadContainerActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- LangSet.setLocale(this)
- ThemeManager(this).applyTheme()
- setContentView(R.layout.activity_container)
-
- val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")
- val fragment = Class.forName(fragmentClassName).newInstance() as Fragment
-
- supportFragmentManager.beginTransaction()
- .replace(R.id.fragment_container, fragment)
- .commit()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
index b3536ea4616..ef7765628cb 100644
--- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
+++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
@@ -15,43 +15,43 @@ class DownloadsManager(private val context: Context) {
private val gson = Gson()
private val downloadsList = loadDownloads().toMutableList()
- val mangaDownloads: List
- get() = downloadsList.filter { it.type == Download.Type.MANGA }
- val animeDownloads: List
- get() = downloadsList.filter { it.type == Download.Type.ANIME }
- val novelDownloads: List
- get() = downloadsList.filter { it.type == Download.Type.NOVEL }
+ val mangaDownloadedTypes: List
+ get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
+ val animeDownloadedTypes: List
+ get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
+ val novelDownloadedTypes: List
+ get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList)
prefs.edit().putString("downloads_key", jsonString).apply()
}
- private fun loadDownloads(): List {
+ private fun loadDownloads(): List {
val jsonString = prefs.getString("downloads_key", null)
return if (jsonString != null) {
- val type = object : TypeToken>() {}.type
+ val type = object : TypeToken>() {}.type
gson.fromJson(jsonString, type)
} else {
emptyList()
}
}
- fun addDownload(download: Download) {
- downloadsList.add(download)
+ fun addDownload(downloadedType: DownloadedType) {
+ downloadsList.add(downloadedType)
saveDownloads()
}
- fun removeDownload(download: Download) {
- downloadsList.remove(download)
- removeDirectory(download)
+ fun removeDownload(downloadedType: DownloadedType) {
+ downloadsList.remove(downloadedType)
+ removeDirectory(downloadedType)
saveDownloads()
}
- fun removeMedia(title: String, type: Download.Type) {
- val subDirectory = if (type == Download.Type.MANGA) {
+ fun removeMedia(title: String, type: DownloadedType.Type) {
+ val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
- } else if (type == Download.Type.ANIME) {
+ } else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
@@ -71,21 +71,31 @@ class DownloadsManager(private val context: Context) {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
cleanDownloads()
}
- downloadsList.removeAll { it.title == title }
+ when (type) {
+ DownloadedType.Type.MANGA -> {
+ downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
+ }
+ DownloadedType.Type.ANIME -> {
+ downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
+ }
+ DownloadedType.Type.NOVEL -> {
+ downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
+ }
+ }
saveDownloads()
}
private fun cleanDownloads() {
- cleanDownload(Download.Type.MANGA)
- cleanDownload(Download.Type.ANIME)
- cleanDownload(Download.Type.NOVEL)
+ cleanDownload(DownloadedType.Type.MANGA)
+ cleanDownload(DownloadedType.Type.ANIME)
+ cleanDownload(DownloadedType.Type.NOVEL)
}
- private fun cleanDownload(type: Download.Type) {
+ private fun cleanDownload(type: DownloadedType.Type) {
// remove all folders that are not in the downloads list
- val subDirectory = if (type == Download.Type.MANGA) {
+ val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
- } else if (type == Download.Type.ANIME) {
+ } else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
@@ -94,18 +104,18 @@ class DownloadsManager(private val context: Context) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory"
)
- val downloadsSubList = if (type == Download.Type.MANGA) {
- mangaDownloads
- } else if (type == Download.Type.ANIME) {
- animeDownloads
+ val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
+ mangaDownloadedTypes
+ } else if (type == DownloadedType.Type.ANIME) {
+ animeDownloadedTypes
} else {
- novelDownloads
+ novelDownloadedTypes
}
if (directory.exists()) {
val files = directory.listFiles()
if (files != null) {
for (file in files) {
- if (!downloadsSubList.any { it.title == file.name }) {
+ if (!downloadsSubLists.any { it.title == file.name }) {
val deleted = file.deleteRecursively()
}
}
@@ -122,11 +132,11 @@ class DownloadsManager(private val context: Context) {
}
}
- fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List) //for debugging
+ fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List) //for debugging
{
val jsonString = gson.toJson(downloadsList)
val file = File(
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
+ context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/downloads.json"
)
if (file.parentFile?.exists() == false) {
@@ -138,25 +148,33 @@ class DownloadsManager(private val context: Context) {
file.writeText(jsonString)
}
- fun queryDownload(download: Download): Boolean {
- return downloadsList.contains(download)
+ fun queryDownload(downloadedType: DownloadedType): Boolean {
+ return downloadsList.contains(downloadedType)
+ }
+
+ fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
+ return if (type == null) {
+ downloadsList.any { it.title == title && it.chapter == chapter }
+ } else {
+ downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
+ }
}
- private fun removeDirectory(download: Download) {
- val directory = if (download.type == Download.Type.MANGA) {
+ private fun removeDirectory(downloadedType: DownloadedType) {
+ val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Manga/${download.title}/${download.chapter}"
+ "Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
- } else if (download.type == Download.Type.ANIME) {
+ } else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Anime/${download.title}/${download.chapter}"
+ "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Novel/${download.title}/${download.chapter}"
+ "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
@@ -173,26 +191,26 @@ class DownloadsManager(private val context: Context) {
}
}
- fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
- val directory = if (download.type == Download.Type.MANGA) {
+ fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
+ val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Manga/${download.title}/${download.chapter}"
+ "Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
- } else if (download.type == Download.Type.ANIME) {
+ } else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Anime/${download.title}/${download.chapter}"
+ "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Novel/${download.title}/${download.chapter}"
+ "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
val destination = File(
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/${download.title}/${download.chapter}"
+ context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
)
if (directory.exists()) {
val copied = directory.copyRecursively(destination, true)
@@ -206,10 +224,10 @@ class DownloadsManager(private val context: Context) {
}
}
- fun purgeDownloads(type: Download.Type) {
- val directory = if (type == Download.Type.MANGA) {
+ fun purgeDownloads(type: DownloadedType.Type) {
+ val directory = if (type == DownloadedType.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
- } else if (type == Download.Type.ANIME) {
+ } else if (type == DownloadedType.Type.ANIME) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
@@ -233,11 +251,51 @@ class DownloadsManager(private val context: Context) {
const val novelLocation = "Dantotsu/Novel"
const val mangaLocation = "Dantotsu/Manga"
const val animeLocation = "Dantotsu/Anime"
+
+ fun getDirectory(context: Context, type: DownloadedType.Type, title: String, chapter: String? = null): File {
+ return if (type == DownloadedType.Type.MANGA) {
+ if (chapter != null) {
+ File(
+ context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "$mangaLocation/$title/$chapter"
+ )
+ } else {
+ File(
+ context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "$mangaLocation/$title"
+ )
+ }
+ } else if (type == DownloadedType.Type.ANIME) {
+ if (chapter != null) {
+ File(
+ context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "$animeLocation/$title/$chapter"
+ )
+ } else {
+ File(
+ context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "$animeLocation/$title"
+ )
+ }
+ } else {
+ if (chapter != null) {
+ File(
+ context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "$novelLocation/$title/$chapter"
+ )
+ } else {
+ File(
+ context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "$novelLocation/$title"
+ )
+ }
+ }
+ }
}
}
-data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
+data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type {
MANGA,
ANIME,
diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
new file mode 100644
index 00000000000..a69ef446d15
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
@@ -0,0 +1,524 @@
+package ani.dantotsu.download.anime
+
+import android.Manifest
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.Environment
+import android.os.IBinder
+import android.widget.Toast
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.offline.DownloadManager
+import androidx.media3.exoplayer.offline.DownloadService
+import ani.dantotsu.FileUrl
+import ani.dantotsu.R
+import ani.dantotsu.currActivity
+import ani.dantotsu.download.DownloadedType
+import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.video.ExoplayerDownloadService
+import ani.dantotsu.download.video.Helper
+import ani.dantotsu.logger
+import ani.dantotsu.media.Media
+import ani.dantotsu.media.SubtitleDownloader
+import ani.dantotsu.media.anime.AnimeWatchFragment
+import ani.dantotsu.parsers.Subtitle
+import ani.dantotsu.parsers.Video
+import ani.dantotsu.snackString
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import com.google.gson.GsonBuilder
+import com.google.gson.InstanceCreator
+import eu.kanade.tachiyomi.animesource.model.SAnime
+import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
+import eu.kanade.tachiyomi.animesource.model.SEpisode
+import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SChapterImpl
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.File
+import java.io.FileOutputStream
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.Queue
+import java.util.concurrent.ConcurrentLinkedQueue
+
+class AnimeDownloaderService : Service() {
+
+ private lateinit var notificationManager: NotificationManagerCompat
+ private lateinit var builder: NotificationCompat.Builder
+ private val downloadsManager: DownloadsManager = Injekt.get()
+
+ private val downloadJobs = mutableMapOf()
+ private val mutex = Mutex()
+ private var isCurrentlyProcessing = false
+ private var currentTasks: MutableList = mutableListOf()
+
+ override fun onBind(intent: Intent?): IBinder? {
+ // This is only required for bound services.
+ return null
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ notificationManager = NotificationManagerCompat.from(this)
+ builder =
+ NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
+ setContentTitle("Anime Download Progress")
+ setSmallIcon(R.drawable.ic_round_download_24)
+ priority = NotificationCompat.PRIORITY_DEFAULT
+ setOnlyAlertOnce(true)
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(
+ NOTIFICATION_ID,
+ builder.build(),
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ )
+ } else {
+ startForeground(NOTIFICATION_ID, builder.build())
+ }
+ ContextCompat.registerReceiver(
+ this,
+ cancelReceiver,
+ IntentFilter(ACTION_CANCEL_DOWNLOAD),
+ ContextCompat.RECEIVER_EXPORTED
+ )
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ AnimeServiceDataSingleton.downloadQueue.clear()
+ downloadJobs.clear()
+ AnimeServiceDataSingleton.isServiceRunning = false
+ unregisterReceiver(cancelReceiver)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ snackString("Download started")
+ val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+ serviceScope.launch {
+ mutex.withLock {
+ if (!isCurrentlyProcessing) {
+ isCurrentlyProcessing = true
+ processQueue()
+ isCurrentlyProcessing = false
+ }
+ }
+ }
+ return START_NOT_STICKY
+ }
+
+ private fun processQueue() {
+ CoroutineScope(Dispatchers.Default).launch {
+ while (AnimeServiceDataSingleton.downloadQueue.isNotEmpty()) {
+ val task = AnimeServiceDataSingleton.downloadQueue.poll()
+ if (task != null) {
+ val job = launch { download(task) }
+ currentTasks.add(task)
+ mutex.withLock {
+ downloadJobs[task.getTaskName()] = job
+ }
+ job.join() // Wait for the job to complete before continuing to the next task
+ mutex.withLock {
+ downloadJobs.remove(task.getTaskName())
+ }
+ updateNotification() // Update the notification after each task is completed
+ }
+ if (AnimeServiceDataSingleton.downloadQueue.isEmpty()) {
+ withContext(Dispatchers.Main) {
+ stopSelf() // Stop the service when the queue is empty
+ }
+ }
+ }
+ }
+ }
+
+ @UnstableApi
+ fun cancelDownload(taskName: String) {
+ val url =
+ AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
+ ?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
+ if (url.isEmpty()) {
+ snackString("Failed to cancel download")
+ return
+ }
+ currentTasks.removeAll { it.getTaskName() == taskName }
+ DownloadService.sendSetStopReason(
+ this@AnimeDownloaderService,
+ ExoplayerDownloadService::class.java,
+ url,
+ androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
+ false
+ )
+ DownloadService.sendRemoveDownload(
+ this@AnimeDownloaderService,
+ ExoplayerDownloadService::class.java,
+ url,
+ false
+ )
+ CoroutineScope(Dispatchers.Default).launch {
+ mutex.withLock {
+ downloadJobs[taskName]?.cancel()
+ downloadJobs.remove(taskName)
+ AnimeServiceDataSingleton.downloadQueue.removeAll { it.getTaskName() == taskName }
+ updateNotification() // Update the notification after cancellation
+ }
+ }
+ }
+
+ private fun updateNotification() {
+ // Update the notification to reflect the current state of the queue
+ val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
+ val text = if (pendingDownloads > 0) {
+ "Pending downloads: $pendingDownloads"
+ } else {
+ "All downloads completed"
+ }
+ builder.setContentText(text)
+ if (ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return
+ }
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
+
+ @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(
+ this@AnimeDownloaderService,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ } else {
+ true
+ }
+
+ builder.setContentText("Downloading ${task.title} - ${task.episode}")
+ if (notifi) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
+
+ broadcastDownloadStarted(task.episode)
+
+ currActivity()?.let {
+ Helper.downloadVideo(
+ it,
+ task.video,
+ task.subtitle
+ )
+ }
+
+ saveMediaInfo(task)
+ task.subtitle?.let {
+ SubtitleDownloader.downloadSubtitle(
+ this@AnimeDownloaderService,
+ it.file.url,
+ DownloadedType(
+ task.title,
+ task.episode,
+ DownloadedType.Type.ANIME,
+ )
+ )
+ }
+ val downloadStarted =
+ hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
+
+ if (!downloadStarted) {
+ logger("Download failed to start")
+ builder.setContentText("${task.title} - ${task.episode} Download failed to start")
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ snackString("${task.title} - ${task.episode} Download failed to start")
+ broadcastDownloadFailed(task.episode)
+ return@withContext
+ }
+
+
+ // periodically check if the download is complete
+ while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
+ val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
+ if (download != null) {
+ if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
+ logger("Download failed")
+ builder.setContentText("${task.title} - ${task.episode} Download failed")
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ snackString("${task.title} - ${task.episode} Download failed")
+ logger("Download failed: ${download.failureReason}")
+ downloadsManager.removeDownload(
+ DownloadedType(
+ task.title,
+ task.episode,
+ DownloadedType.Type.ANIME,
+ )
+ )
+ FirebaseCrashlytics.getInstance().recordException(
+ Exception(
+ "Anime Download failed:" +
+ " ${download.failureReason}" +
+ " url: ${task.video.file.url}" +
+ " title: ${task.title}" +
+ " episode: ${task.episode}"
+ )
+ )
+ currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
+ broadcastDownloadFailed(task.episode)
+ break
+ }
+ if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
+ logger("Download completed")
+ builder.setContentText("${task.title} - ${task.episode} Download completed")
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ snackString("${task.title} - ${task.episode} Download completed")
+ getSharedPreferences(
+ getString(R.string.anime_downloads),
+ Context.MODE_PRIVATE
+ ).edit().putString(
+ task.getTaskName(),
+ task.video.file.url
+ ).apply()
+ downloadsManager.addDownload(
+ DownloadedType(
+ task.title,
+ task.episode,
+ DownloadedType.Type.ANIME,
+ )
+ )
+ currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
+ broadcastDownloadFinished(task.episode)
+ break
+ }
+ if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
+ logger("Download stopped")
+ builder.setContentText("${task.title} - ${task.episode} Download stopped")
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ snackString("${task.title} - ${task.episode} Download stopped")
+ break
+ }
+ broadcastDownloadProgress(
+ task.episode,
+ download.percentDownloaded.toInt()
+ )
+ if (notifi) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
+ }
+ kotlinx.coroutines.delay(2000)
+ }
+ }
+ } catch (e: Exception) {
+ if (e.message?.contains("Coroutine was cancelled") == false) { //wut
+ logger("Exception while downloading file: ${e.message}")
+ snackString("Exception while downloading file: ${e.message}")
+ e.printStackTrace()
+ FirebaseCrashlytics.getInstance().recordException(e)
+ }
+ broadcastDownloadFailed(task.episode)
+ }
+ }
+
+ @androidx.annotation.OptIn(UnstableApi::class)
+ suspend fun hasDownloadStarted(
+ downloadManager: DownloadManager,
+ task: AnimeDownloadTask,
+ timeout: Long
+ ): Boolean {
+ val startTime = System.currentTimeMillis()
+ while (System.currentTimeMillis() - startTime < timeout) {
+ val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
+ if (download != null) {
+ return true
+ }
+ // Delay between each poll
+ kotlinx.coroutines.delay(500)
+ }
+ return false
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ private fun saveMediaInfo(task: AnimeDownloadTask) {
+ GlobalScope.launch(Dispatchers.IO) {
+ val directory = File(
+ getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "${DownloadsManager.animeLocation}/${task.title}"
+ )
+ val episodeDirectory = File(directory, task.episode)
+ if (!directory.exists()) directory.mkdirs()
+ if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
+
+ val file = File(directory, "media.json")
+ val gson = GsonBuilder()
+ .registerTypeAdapter(SChapter::class.java, InstanceCreator {
+ SChapterImpl() // Provide an instance of SChapterImpl
+ })
+ .registerTypeAdapter(SAnime::class.java, InstanceCreator {
+ SAnimeImpl() // Provide an instance of SAnimeImpl
+ })
+ .registerTypeAdapter(SEpisode::class.java, InstanceCreator {
+ SEpisodeImpl() // Provide an instance of SEpisodeImpl
+ })
+ .create()
+ val mediaJson = gson.toJson(task.sourceMedia)
+ val media = gson.fromJson(mediaJson, Media::class.java)
+ if (media != null) {
+ media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
+ media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
+ if (task.episodeImage != null) {
+ media.anime?.episodes?.get(task.episode)?.let { episode ->
+ episode.thumb = downloadImage(
+ task.episodeImage,
+ episodeDirectory,
+ "episodeImage.jpg"
+ )?.let {
+ FileUrl(
+ it
+ )
+ }
+ }
+ downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg")
+ }
+
+ val jsonString = gson.toJson(media)
+ withContext(Dispatchers.Main) {
+ file.writeText(jsonString)
+ }
+ }
+ }
+ }
+
+
+ private suspend fun downloadImage(url: String, directory: File, name: String): String? =
+ withContext(Dispatchers.IO) {
+ var connection: HttpURLConnection? = null
+ println("Downloading url $url")
+ try {
+ connection = URL(url).openConnection() as HttpURLConnection
+ connection.connect()
+ if (connection.responseCode != HttpURLConnection.HTTP_OK) {
+ throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
+ }
+
+ val file = File(directory, name)
+ FileOutputStream(file).use { output ->
+ connection.inputStream.use { input ->
+ input.copyTo(output)
+ }
+ }
+ return@withContext file.absolutePath
+ } catch (e: Exception) {
+ e.printStackTrace()
+ withContext(Dispatchers.Main) {
+ Toast.makeText(
+ this@AnimeDownloaderService,
+ "Exception while saving ${name}: ${e.message}",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ null
+ } finally {
+ connection?.disconnect()
+ }
+ }
+
+ private fun broadcastDownloadStarted(episodeNumber: String) {
+ val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply {
+ putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
+ }
+ sendBroadcast(intent)
+ }
+
+ private fun broadcastDownloadFinished(episodeNumber: String) {
+ val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FINISHED).apply {
+ putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
+ }
+ sendBroadcast(intent)
+ }
+
+ private fun broadcastDownloadFailed(episodeNumber: String) {
+ val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FAILED).apply {
+ putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
+ }
+ sendBroadcast(intent)
+ }
+
+ private fun broadcastDownloadProgress(episodeNumber: String, progress: Int) {
+ val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_PROGRESS).apply {
+ putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
+ putExtra("progress", progress)
+ }
+ sendBroadcast(intent)
+ }
+
+ private val cancelReceiver = object : BroadcastReceiver() {
+ @androidx.annotation.OptIn(UnstableApi::class)
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == ACTION_CANCEL_DOWNLOAD) {
+ val taskName = intent.getStringExtra(EXTRA_TASK_NAME)
+ taskName?.let {
+ cancelDownload(it)
+ }
+ }
+ }
+ }
+
+
+ data class AnimeDownloadTask(
+ val title: String,
+ val episode: String,
+ val video: Video,
+ val subtitle: Subtitle? = null,
+ val sourceMedia: Media? = null,
+ val episodeImage: String? = null,
+ val retries: Int = 2,
+ val simultaneousDownloads: Int = 2,
+ ) {
+ fun getTaskName(): String {
+ return "$title - $episode"
+ }
+
+ companion object {
+ fun getTaskName(title: String, episode: String): String {
+ return "$title - $episode"
+ }
+ }
+ }
+
+ companion object {
+ private const val NOTIFICATION_ID = 1103
+ const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
+ const val EXTRA_TASK_NAME = "extra_task_name"
+ }
+}
+
+object AnimeServiceDataSingleton {
+ var video: Video? = null
+ var sourceMedia: Media? = null
+ var downloadQueue: Queue = ConcurrentLinkedQueue()
+
+ @Volatile
+ var isServiceRunning: Boolean = false
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeAdapter.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeAdapter.kt
new file mode 100644
index 00000000000..4cd57f050ff
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeAdapter.kt
@@ -0,0 +1,112 @@
+package ani.dantotsu.download.anime
+
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.cardview.widget.CardView
+import ani.dantotsu.R
+
+
+class OfflineAnimeAdapter(
+ private val context: Context,
+ private var items: List,
+ private val searchListener: OfflineAnimeSearchListener
+) : BaseAdapter() {
+ private val inflater: LayoutInflater =
+ context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ private var originalItems: List = items
+ private var style =
+ context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
+
+ override fun getCount(): Int {
+ return items.size
+ }
+
+ override fun getItem(position: Int): Any {
+ return items[position]
+ }
+
+ override fun getItemId(position: Int): Long {
+ return position.toLong()
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
+
+ val view: View = convertView ?: when (style) {
+ 0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
+ 1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
+ else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
+ }
+
+ val item = getItem(position) as OfflineAnimeModel
+ val imageView = view.findViewById(R.id.itemCompactImage)
+ val titleTextView = view.findViewById(R.id.itemCompactTitle)
+ val itemScore = view.findViewById(R.id.itemCompactScore)
+ val itemScoreBG = view.findViewById(R.id.itemCompactScoreBG)
+ val ongoing = view.findViewById(R.id.itemCompactOngoing)
+ val totalepisodes = view.findViewById(R.id.itemCompactTotal)
+ val typeimage = view.findViewById(R.id.itemCompactTypeImage)
+ val type = view.findViewById(R.id.itemCompactRelation)
+ val typeView = view.findViewById(R.id.itemCompactType)
+
+ if (style == 0) {
+ val bannerView = view.findViewById(R.id.itemCompactBanner) // for large view
+ val episodes = view.findViewById(R.id.itemTotal)
+ episodes.text = " Episodes"
+ bannerView.setImageURI(item.banner)
+ totalepisodes.text = item.totalEpisodeList
+ } else if (style == 1) {
+ val watchedEpisodes =
+ view.findViewById(R.id.itemCompactUserProgress) // for compact view
+ watchedEpisodes.text = item.watchedEpisode
+ totalepisodes.text = " | " + item.totalEpisode
+ }
+
+ // Bind item data to the views
+ typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
+ type.text = item.type
+ typeView.visibility = View.VISIBLE
+ imageView.setImageURI(item.image)
+ titleTextView.text = item.title
+ itemScore.text = item.score
+
+ if (item.isOngoing) {
+ ongoing.visibility = View.VISIBLE
+ } else {
+ ongoing.visibility = View.GONE
+ }
+ return view
+ }
+
+ fun onSearchQuery(query: String) {
+ // Implement the filtering logic here, for example:
+ items = if (query.isEmpty()) {
+ // Return the original list if the query is empty
+ originalItems
+ } else {
+ // Filter the list based on the query
+ originalItems.filter { it.title.contains(query, ignoreCase = true) }
+ }
+ notifyDataSetChanged() // Notify the adapter that the data set has changed
+ }
+
+ fun setItems(items: List) {
+ this.items = items
+ this.originalItems = items
+ notifyDataSetChanged()
+ }
+
+ fun notifyNewGrid() {
+ style =
+ context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
+ notifyDataSetChanged()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
new file mode 100644
index 00000000000..99888856e6b
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
@@ -0,0 +1,450 @@
+package ani.dantotsu.download.anime
+
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.text.Editable
+import android.text.TextWatcher
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.AlphaAnimation
+import android.view.animation.LayoutAnimationController
+import android.view.animation.OvershootInterpolator
+import android.widget.AbsListView
+import android.widget.AutoCompleteTextView
+import android.widget.GridView
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.OptIn
+import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
+import androidx.core.view.marginBottom
+import androidx.fragment.app.Fragment
+import androidx.media3.common.util.UnstableApi
+import ani.dantotsu.R
+import ani.dantotsu.bottomBar
+import ani.dantotsu.currActivity
+import ani.dantotsu.currContext
+import ani.dantotsu.download.DownloadedType
+import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.initActivity
+import ani.dantotsu.loadData
+import ani.dantotsu.logger
+import ani.dantotsu.media.Media
+import ani.dantotsu.media.MediaDetailsActivity
+import ani.dantotsu.navBarHeight
+import ani.dantotsu.setSafeOnClickListener
+import ani.dantotsu.settings.SettingsDialogFragment
+import ani.dantotsu.settings.UserInterfaceSettings
+import ani.dantotsu.snackString
+import ani.dantotsu.statusBarHeight
+import com.google.android.material.card.MaterialCardView
+import com.google.android.material.imageview.ShapeableImageView
+import com.google.android.material.textfield.TextInputLayout
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import com.google.gson.GsonBuilder
+import com.google.gson.InstanceCreator
+import eu.kanade.tachiyomi.animesource.model.SAnime
+import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
+import eu.kanade.tachiyomi.animesource.model.SEpisode
+import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SChapterImpl
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.File
+import kotlin.math.max
+import kotlin.math.min
+
+class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
+
+ private val downloadManager = Injekt.get()
+ private var downloads: List = listOf()
+ private lateinit var gridView: GridView
+ private lateinit var adapter: OfflineAnimeAdapter
+ private lateinit var total : TextView
+ private var uiSettings: UserInterfaceSettings =
+ loadData("ui_settings") ?: UserInterfaceSettings()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
+
+ val textInputLayout = view.findViewById(R.id.offlineMangaSearchBar)
+ textInputLayout.hint = "Anime"
+ val currentColor = textInputLayout.boxBackgroundColor
+ val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
+ textInputLayout.boxBackgroundColor = semiTransparentColor
+ val materialCardView = view.findViewById(R.id.offlineMangaAvatarContainer)
+ materialCardView.setCardBackgroundColor(semiTransparentColor)
+ val typedValue = TypedValue()
+ requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
+ val color = typedValue.data
+
+ val animeUserAvatar = view.findViewById(R.id.offlineMangaUserAvatar)
+ animeUserAvatar.setSafeOnClickListener {
+ val dialogFragment =
+ SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineANIME)
+ dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
+ }
+ if (!uiSettings.immersiveMode) {
+ view.rootView.fitsSystemWindows = true
+ }
+ val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getBoolean("colorOverflow", false) ?: false
+ if (!colorOverflow) {
+ textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
+ materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
+ }
+
+ val searchView = view.findViewById(R.id.animeSearchBarText)
+ searchView.addTextChangedListener(object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
+ }
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ onSearchQuery(s.toString())
+ }
+ })
+ var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getInt("offline_view", 0)
+ val layoutList = view.findViewById(R.id.downloadedList)
+ val layoutcompact = view.findViewById(R.id.downloadedGrid)
+ var selected = when (style) {
+ 0 -> layoutList
+ 1 -> layoutcompact
+ else -> layoutList
+ }
+ selected.alpha = 1f
+
+ fun selected(it: ImageView) {
+ selected.alpha = 0.33f
+ selected = it
+ selected.alpha = 1f
+ }
+
+ layoutList.setOnClickListener {
+ selected(it as ImageView)
+ style = 0
+ context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
+ ?.putInt("offline_view", style!!)?.apply()
+ gridView.visibility = View.GONE
+ gridView = view.findViewById(R.id.gridView)
+ adapter.notifyNewGrid()
+ grid()
+ }
+
+ layoutcompact.setOnClickListener {
+ selected(it as ImageView)
+ style = 1
+ context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
+ ?.putInt("offline_view", style!!)?.apply()
+ gridView.visibility = View.GONE
+ gridView = view.findViewById(R.id.gridView1)
+ adapter.notifyNewGrid()
+ grid()
+ }
+
+ gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
+ total = view.findViewById(R.id.total)
+ grid()
+ return view
+ }
+
+ @OptIn(UnstableApi::class)
+ private fun grid() {
+ gridView.visibility = View.VISIBLE
+ getDownloads()
+ val fadeIn = AlphaAnimation(0f, 1f)
+ fadeIn.duration = 300 // animations pog
+ gridView.layoutAnimation = LayoutAnimationController(fadeIn)
+ adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
+ gridView.adapter = adapter
+ gridView.scheduleLayoutAnimation()
+ total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
+ gridView.setOnItemClickListener { _, _, position, _ ->
+ // Get the OfflineAnimeModel that was clicked
+ val item = adapter.getItem(position) as OfflineAnimeModel
+ val media =
+ downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
+ media?.let {
+ val mediaModel = getMedia(it)
+ if (mediaModel == null) {
+ snackString("Error loading media.json")
+ return@let
+ }
+ MediaDetailsActivity.mediaSingleton = mediaModel
+ startActivity(
+ Intent(requireContext(), MediaDetailsActivity::class.java)
+ .putExtra("download", true)
+ )
+ } ?: run {
+ snackString("no media found")
+ }
+ }
+ gridView.setOnItemLongClickListener { _, _, position, _ ->
+ // Get the OfflineAnimeModel that was clicked
+ val item = adapter.getItem(position) as OfflineAnimeModel
+ val type: DownloadedType.Type =
+ DownloadedType.Type.ANIME
+
+ // Alert dialog to confirm deletion
+ val builder =
+ androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
+ builder.setTitle("Delete ${item.title}?")
+ builder.setMessage("Are you sure you want to delete ${item.title}?")
+ builder.setPositiveButton("Yes") { _, _ ->
+ downloadManager.removeMedia(item.title, type)
+ val mediaIds = requireContext().getSharedPreferences(
+ getString(R.string.anime_downloads),
+ Context.MODE_PRIVATE
+ )
+ ?.all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
+ if (mediaIds.isEmpty()) {
+ snackString("No media found") // if this happens, terrible things have happened
+ }
+ for (mediaId in mediaIds) {
+ ani.dantotsu.download.video.Helper.downloadManager(requireContext())
+ .removeDownload(mediaId.toString())
+ }
+ getDownloads()
+ adapter.setItems(downloads)
+ total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
+ }
+ builder.setNegativeButton("No") { _, _ ->
+ // Do nothing
+ }
+ val dialog = builder.show()
+ dialog.window?.setDimAmount(0.8f)
+ true
+ }
+ }
+
+ override fun onSearchQuery(query: String) {
+ adapter.onSearchQuery(query)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ var height = statusBarHeight
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
+ if (displayCutout != null) {
+ if (displayCutout.boundingRects.size > 0) {
+ height = max(
+ statusBarHeight,
+ min(
+ displayCutout.boundingRects[0].width(),
+ displayCutout.boundingRects[0].height()
+ )
+ )
+ }
+ }
+ }
+ val scrollTop = view.findViewById(R.id.mangaPageScrollTop)
+ scrollTop.translationY =
+ -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
+ val visible = false
+
+ fun animate() {
+ val start = if (visible) 0f else 1f
+ val end = if (!visible) 0f else 1f
+ ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
+ duration = 300
+ interpolator = OvershootInterpolator(2f)
+ start()
+ }
+ ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
+ duration = 300
+ interpolator = OvershootInterpolator(2f)
+ start()
+ }
+ }
+
+ scrollTop.setOnClickListener {
+ gridView.smoothScrollToPositionFromTop(0, 0)
+ }
+
+ // Assuming 'scrollTop' is a view that you want to hide/show
+ scrollTop.visibility = View.GONE
+
+ gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
+ override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
+ // Implement behavior for different scroll states if needed
+ }
+
+ override fun onScroll(
+ view: AbsListView,
+ firstVisibleItem: Int,
+ visibleItemCount: Int,
+ totalItemCount: Int
+ ) {
+ val first = view.getChildAt(0)
+ val visibility = first != null && first.top < -height
+ scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
+ }
+ })
+ initActivity(requireActivity())
+
+ }
+
+ override fun onResume() {
+ super.onResume()
+ getDownloads()
+ adapter.notifyDataSetChanged()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ downloads = listOf()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ downloads = listOf()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ downloads = listOf()
+ }
+
+ private fun getDownloads() {
+ downloads = listOf()
+ val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
+ val newAnimeDownloads = mutableListOf()
+ for (title in animeTitles) {
+ val _downloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
+ val download = _downloads.first()
+ val offlineAnimeModel = loadOfflineAnimeModel(download)
+ newAnimeDownloads += offlineAnimeModel
+ }
+ downloads = newAnimeDownloads
+ }
+
+ private fun getMedia(downloadedType: DownloadedType): Media? {
+ val type = if (downloadedType.type == DownloadedType.Type.ANIME) {
+ "Anime"
+ } else if (downloadedType.type == DownloadedType.Type.MANGA) {
+ "Manga"
+ } else {
+ "Novel"
+ }
+ val directory = File(
+ currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "Dantotsu/$type/${downloadedType.title}"
+ )
+ //load media.json and convert to media class with gson
+ return try {
+ val gson = GsonBuilder()
+ .registerTypeAdapter(SChapter::class.java, InstanceCreator {
+ SChapterImpl() // Provide an instance of SChapterImpl
+ })
+ .registerTypeAdapter(SAnime::class.java, InstanceCreator {
+ SAnimeImpl() // Provide an instance of SAnimeImpl
+ })
+ .registerTypeAdapter(SEpisode::class.java, InstanceCreator {
+ SEpisodeImpl() // Provide an instance of SEpisodeImpl
+ })
+ .create()
+ val media = File(directory, "media.json")
+ val mediaJson = media.readText()
+ gson.fromJson(mediaJson, Media::class.java)
+ } catch (e: Exception) {
+ logger("Error loading media.json: ${e.message}")
+ logger(e.printStackTrace())
+ FirebaseCrashlytics.getInstance().recordException(e)
+ null
+ }
+ }
+
+ private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
+ val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
+ "Manga"
+ } else if (downloadedType.type == DownloadedType.Type.ANIME) {
+ "Anime"
+ } else {
+ "Novel"
+ }
+ val directory = File(
+ currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "Dantotsu/$type/${downloadedType.title}"
+ )
+ //load media.json and convert to media class with gson
+ try {
+ val media = File(directory, "media.json")
+ val mediaJson = media.readText()
+ val mediaModel = getMedia(downloadedType)!!
+ val cover = File(directory, "cover.jpg")
+ val coverUri: Uri? = if (cover.exists()) {
+ Uri.fromFile(cover)
+ } else null
+ val banner = File(directory, "banner.jpg")
+ val bannerUri: Uri? = if (banner.exists()) {
+ Uri.fromFile(banner)
+ } else null
+ val title = mediaModel.mainName()
+ val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
+ ?: 0) else mediaModel.userScore) / 10.0).toString()
+ val isOngoing =
+ mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
+ val isUserScored = mediaModel.userScore != 0
+ val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
+ val totalEpisode =
+ if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
+ ?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
+ val chapters = " Chapters"
+ val totalEpisodesList =
+ if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
+ ?: "~").toString()
+ return OfflineAnimeModel(
+ title,
+ score,
+ totalEpisode,
+ totalEpisodesList,
+ watchedEpisodes,
+ type,
+ chapters,
+ isOngoing,
+ isUserScored,
+ coverUri,
+ bannerUri
+ )
+ } catch (e: Exception) {
+ logger("Error loading media.json: ${e.message}")
+ logger(e.printStackTrace())
+ FirebaseCrashlytics.getInstance().recordException(e)
+ return OfflineAnimeModel(
+ "unknown",
+ "0",
+ "??",
+ "??",
+ "??",
+ "movie",
+ "hmm",
+ false,
+ false,
+ null,
+ null
+ )
+ }
+ }
+}
+
+interface OfflineAnimeSearchListener {
+ fun onSearchQuery(query: String)
+}
diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeModel.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeModel.kt
new file mode 100644
index 00000000000..4823e2afc91
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeModel.kt
@@ -0,0 +1,17 @@
+package ani.dantotsu.download.anime
+
+import android.net.Uri
+
+data class OfflineAnimeModel(
+ val title: String,
+ val score: String,
+ val totalEpisode: String,
+ val totalEpisodeList: String,
+ val watchedEpisode: String,
+ val type: String,
+ val episodes: String,
+ val isOngoing: Boolean,
+ val isUserScored: Boolean,
+ val image: Uri?,
+ val banner: Uri?,
+)
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
index fce89d3ee2b..0a1337dfebc 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
@@ -18,7 +18,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
-import ani.dantotsu.download.Download
+import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
@@ -187,7 +187,8 @@ class MangaDownloaderService : Service() {
true
}
- val deferredList = mutableListOf>()
+ //val deferredList = mutableListOf>()
+ val deferredMap = mutableMapOf>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
@@ -196,15 +197,12 @@ class MangaDownloaderService : Service() {
// Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
- // Limit the number of simultaneous downloads from the task
- if (deferredList.size >= task.simultaneousDownloads) {
- // Wait for all deferred to complete and clear the list
- deferredList.awaitAll()
- deferredList.clear()
+ if (deferredMap.size >= task.simultaneousDownloads) {
+ deferredMap.values.awaitAll()
+ deferredMap.clear()
}
- // Download the image and add to deferred list
- val deferred = async(Dispatchers.IO) {
+ deferredMap[index] = async(Dispatchers.IO) {
var bitmap: Bitmap? = null
var retryCount = 0
@@ -217,7 +215,6 @@ class MangaDownloaderService : Service() {
retryCount++
}
- // Cache the image if successful
if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
}
@@ -233,12 +230,10 @@ class MangaDownloaderService : Service() {
bitmap
}
-
- deferredList.add(deferred)
}
// Wait for any remaining deferred to complete
- deferredList.awaitAll()
+ deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
@@ -246,10 +241,10 @@ class MangaDownloaderService : Service() {
saveMediaInfo(task)
downloadsManager.addDownload(
- Download(
+ DownloadedType(
task.title,
task.chapter,
- Download.Type.MANGA
+ DownloadedType.Type.MANGA
)
)
broadcastDownloadFinished(task.chapter)
@@ -314,7 +309,16 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
- file.writeText(jsonString)
+ try {
+ file.writeText(jsonString)
+ } catch (e: android.system.ErrnoException) {
+ e.printStackTrace()
+ Toast.makeText(
+ this@MangaDownloaderService,
+ "Error while saving: ${e.localizedMessage}",
+ Toast.LENGTH_LONG
+ ).show()
+ }
}
}
}
diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt
index b91bb55abec..3bacbcb2afd 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt
@@ -1,11 +1,13 @@
package ani.dantotsu.download.manga
+import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
+import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
@@ -19,6 +21,8 @@ class OfflineMangaAdapter(
private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List = items
+ private var style =
+ context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
override fun getCount(): Int {
return items.size
@@ -32,23 +36,47 @@ class OfflineMangaAdapter(
return position.toLong()
}
+ @SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
- var view = convertView
- if (view == null) {
- view = inflater.inflate(R.layout.item_media_compact, parent, false)
+
+ val view: View = convertView ?: when (style) {
+ 0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
+ 1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
+ else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
}
val item = getItem(position) as OfflineMangaModel
- val imageView = view!!.findViewById(R.id.itemCompactImage)
+ val imageView = view.findViewById(R.id.itemCompactImage)
val titleTextView = view.findViewById(R.id.itemCompactTitle)
val itemScore = view.findViewById(R.id.itemCompactScore)
val itemScoreBG = view.findViewById(R.id.itemCompactScoreBG)
val ongoing = view.findViewById(R.id.itemCompactOngoing)
+ val totalchapter = view.findViewById(R.id.itemCompactTotal)
+ val typeimage = view.findViewById(R.id.itemCompactTypeImage)
+ val type = view.findViewById(R.id.itemCompactRelation)
+ val typeView = view.findViewById(R.id.itemCompactType)
+
+ if (style == 0) {
+ val bannerView = view.findViewById(R.id.itemCompactBanner) // for large view
+ val chapters = view.findViewById(R.id.itemTotal)
+ chapters.text = " Chapters"
+ bannerView.setImageURI(item.banner)
+ totalchapter.text = item.totalChapter
+ } else if (style == 1) {
+ val readchapter =
+ view.findViewById(R.id.itemCompactUserProgress) // for compact view
+ readchapter.text = item.readChapter
+ totalchapter.text = " | " + item.totalChapter
+ }
+
// Bind item data to the views
- // For example:
+ typeimage.setImageResource(if (item.type == "Novel") R.drawable.ic_round_book_24 else R.drawable.ic_round_import_contacts_24)
+ type.text = item.type
+ typeView.visibility = View.VISIBLE
imageView.setImageURI(item.image)
titleTextView.text = item.title
itemScore.text = item.score
+
if (item.isOngoing) {
ongoing.visibility = View.VISIBLE
} else {
@@ -74,4 +102,10 @@ class OfflineMangaAdapter(
this.originalItems = items
notifyDataSetChanged()
}
+
+ fun notifyNewGrid() {
+ style =
+ context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
+ notifyDataSetChanged()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
index e81d2817cf1..8f1d2b06b4a 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
@@ -13,21 +13,33 @@ import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.AlphaAnimation
+import android.view.animation.LayoutAnimationController
import android.view.animation.OvershootInterpolator
+import android.widget.AbsListView
import android.widget.AutoCompleteTextView
import android.widget.GridView
+import android.widget.ImageView
+import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
+import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import ani.dantotsu.R
+import ani.dantotsu.bottomBar
+import ani.dantotsu.currActivity
import ani.dantotsu.currContext
-import ani.dantotsu.download.Download
+import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.initActivity
+import ani.dantotsu.loadData
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
+import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
+import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView
@@ -45,19 +57,24 @@ import kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
+
private val downloadManager = Injekt.get()
private var downloads: List = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
+ private lateinit var total : TextView
+ private var uiSettings: UserInterfaceSettings =
+ loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
+ val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
val textInputLayout = view.findViewById(R.id.offlineMangaSearchBar)
+ textInputLayout.hint = "Manga"
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
@@ -69,16 +86,13 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val animeUserAvatar = view.findViewById(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
- animeUserAvatar.setSafeOnClickListener {
- val dialogFragment = SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
- dialogFragment.show(
- (it.context as AppCompatActivity).supportFragmentManager,
- "dialog"
- )
- }
-
+ val dialogFragment =
+ SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
+ dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
+ }
+ if (!uiSettings.immersiveMode) {
+ view.rootView.fitsSystemWindows = true
}
-
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
@@ -98,17 +112,65 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
onSearchQuery(s.toString())
}
})
+ var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getInt("offline_view", 0)
+ val layoutList = view.findViewById(R.id.downloadedList)
+ val layoutcompact = view.findViewById(R.id.downloadedGrid)
+ var selected = when (style) {
+ 0 -> layoutList
+ 1 -> layoutcompact
+ else -> layoutList
+ }
+ selected.alpha = 1f
+
+ fun selected(it: ImageView) {
+ selected.alpha = 0.33f
+ selected = it
+ selected.alpha = 1f
+ }
+
+ layoutList.setOnClickListener {
+ selected(it as ImageView)
+ style = 0
+ requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("offline_view", style!!).apply()
+ gridView.visibility = View.GONE
+ gridView = view.findViewById(R.id.gridView)
+ adapter.notifyNewGrid()
+ grid()
+
+ }
+
+ layoutcompact.setOnClickListener {
+ selected(it as ImageView)
+ style = 1
+ requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("offline_view", style!!).apply()
+ gridView.visibility = View.GONE
+ gridView = view.findViewById(R.id.gridView1)
+ adapter.notifyNewGrid()
+ grid()
+ }
+ gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
+ total = view.findViewById(R.id.total)
+ grid()
+ return view
+ }
- gridView = view.findViewById(R.id.gridView)
+ private fun grid() {
+ gridView.visibility = View.VISIBLE
getDownloads()
+ val fadeIn = AlphaAnimation(0f, 1f)
+ fadeIn.duration = 300 // animations pog
+ gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
gridView.adapter = adapter
- gridView.setOnItemClickListener { parent, view, position, id ->
+ gridView.scheduleLayoutAnimation()
+ total.text = if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
+ gridView.setOnItemClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media =
- downloadManager.mangaDownloads.firstOrNull { it.title == item.title }
- ?: downloadManager.novelDownloads.firstOrNull { it.title == item.title }
+ downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
+ ?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java)
@@ -120,32 +182,33 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
- gridView.setOnItemLongClickListener { parent, view, position, id ->
+ gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
- val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) {
- Download.Type.MANGA
- } else {
- Download.Type.NOVEL
- }
+ val type: DownloadedType.Type =
+ if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
+ DownloadedType.Type.MANGA
+ } else {
+ DownloadedType.Type.NOVEL
+ }
// Alert dialog to confirm deletion
- val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
+ val builder =
+ androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
getDownloads()
adapter.setItems(downloads)
+ total.text = if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
- val dialog = builder.show()
+ val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true
}
-
- return view
}
override fun onSearchQuery(query: String) {
@@ -154,6 +217,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ initActivity(requireActivity())
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
@@ -170,7 +234,10 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
val scrollTop = view.findViewById(R.id.mangaPageScrollTop)
- var visible = false
+ scrollTop.translationY =
+ -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
+ val visible = false
+
fun animate() {
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
@@ -187,9 +254,30 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
scrollTop.setOnClickListener {
- //TODO: scroll to top
+ gridView.smoothScrollToPositionFromTop(0, 0)
}
+ // Assuming 'scrollTop' is a view that you want to hide/show
+ scrollTop.visibility = View.GONE
+
+ gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
+ override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
+ // Implement behavior for different scroll states if needed
+ }
+
+ override fun onScroll(
+ view: AbsListView,
+ firstVisibleItem: Int,
+ visibleItemCount: Int,
+ totalItemCount: Int
+ ) {
+ val first = view.getChildAt(0)
+ val visibility = first != null && first.top < -height
+ scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
+ }
+ })
+
+
}
override fun onResume() {
@@ -215,19 +303,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() {
downloads = listOf()
- val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct()
+ val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf()
for (title in mangaTitles) {
- val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
+ val _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = newMangaDownloads
- val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct()
+ val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf()
for (title in novelTitles) {
- val _downloads = downloadManager.novelDownloads.filter { it.title == title }
+ val _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
@@ -236,17 +324,17 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
- private fun getMedia(download: Download): Media? {
- val type = if (download.type == Download.Type.MANGA) {
+ private fun getMedia(downloadedType: DownloadedType): Media? {
+ val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
- } else if (download.type == Download.Type.ANIME) {
+ } else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/$type/${download.title}"
+ "Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try {
@@ -266,44 +354,72 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
- private fun loadOfflineMangaModel(download: Download): OfflineMangaModel {
- val type = if (download.type == Download.Type.MANGA) {
+ private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
+ val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
- } else if (download.type == Download.Type.ANIME) {
+ } else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/$type/${download.title}"
+ "Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val media = File(directory, "media.json")
val mediaJson = media.readText()
- val mediaModel = getMedia(download)!!
+ val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
- } else {
- null
- }
- val title = mediaModel.nameMAL ?: mediaModel.nameRomaji
+ } else null
+ val banner = File(directory, "banner.jpg")
+ val bannerUri: Uri? = if (banner.exists()) {
+ Uri.fromFile(banner)
+ } else null
+ val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
- val isOngoing = false
+ val isOngoing =
+ mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
- return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
+ val readchapter = (mediaModel.userProgress ?: "~").toString()
+ val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
+ val chapters = " Chapters"
+ return OfflineMangaModel(
+ title,
+ score,
+ totalchapter,
+ readchapter,
+ type,
+ chapters,
+ isOngoing,
+ isUserScored,
+ coverUri,
+ bannerUri
+ )
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
- return OfflineMangaModel("unknown", "0", false, false, null)
+ return OfflineMangaModel(
+ "unknown",
+ "0",
+ "??",
+ "??",
+ "movie",
+ "hmm",
+ false,
+ false,
+ null,
+ null
+ )
}
}
}
interface OfflineMangaSearchListener {
fun onSearchQuery(query: String)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt
index 568081ee0d2..e9693a5b83f 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt
@@ -5,7 +5,12 @@ import android.net.Uri
data class OfflineMangaModel(
val title: String,
val score: String,
+ val totalChapter: String,
+ val readChapter: String,
+ val type: String,
+ val chapters: String,
val isOngoing: Boolean,
val isUserScored: Boolean,
- val image: Uri?
+ val image: Uri?,
+ val banner: Uri?
)
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
index e987cd253f2..d729ef5789c 100644
--- a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
@@ -17,7 +17,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
-import ani.dantotsu.download.Download
+import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
@@ -330,10 +330,10 @@ class NovelDownloaderService : Service() {
saveMediaInfo(task)
downloadsManager.addDownload(
- Download(
+ DownloadedType(
task.title,
task.chapter,
- Download.Type.NOVEL
+ DownloadedType.Type.NOVEL
)
)
broadcastDownloadFinished(task.originalLink)
diff --git a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt b/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt
similarity index 91%
rename from app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt
rename to app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt
index 3a26ac1d867..4add998c8c7 100644
--- a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt
+++ b/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt
@@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R
@UnstableApi
-class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) {
+class ExoplayerDownloadService :
+ DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
companion object {
private const val JOB_ID = 1
private const val FOREGROUND_NOTIFICATION_ID = 1
diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt
index 2dcf31c641b..25d0d476697 100644
--- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt
+++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt
@@ -1,8 +1,19 @@
package ani.dantotsu.download.video
+import android.Manifest
import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.AlertDialog
import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
import android.net.Uri
+import android.os.Build
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.getString
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
@@ -15,6 +26,7 @@ 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
@@ -22,7 +34,12 @@ import androidx.media3.exoplayer.scheduler.Requirements
import androidx.media3.ui.TrackSelectionDialogBuilder
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.okHttpClient
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
@@ -37,6 +54,8 @@ import java.util.concurrent.*
object Helper {
+ private var simpleCache: SimpleCache? = null
+
@SuppressLint("UnsafeOptInUsageError")
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory {
@@ -82,27 +101,13 @@ object Helper {
)
downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) {
- TrackSelectionDialogBuilder(
- context, "Select thingy", helper.getTracks(0).groups
- ) { _, overrides ->
- val params = TrackSelectionParameters.Builder(context)
- overrides.forEach {
- params.addOverride(it.value)
- }
- helper.addTrackSelection(0, params.build())
- MyDownloadService
+ helper.getDownloadRequest(null).let {
DownloadService.sendAddDownload(
context,
- MyDownloadService::class.java,
- helper.getDownloadRequest(null),
+ ExoplayerDownloadService::class.java,
+ it,
false
)
- }.apply {
- setTheme(R.style.DialogTheme)
- setTrackNameProvider {
- if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
- }
- build().show()
}
}
@@ -114,13 +119,13 @@ object Helper {
private var download: DownloadManager? = null
- private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads"
+ private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Synchronized
@UnstableApi
fun downloadManager(context: Context): DownloadManager {
return download ?: let {
- val database = StandaloneDatabaseProvider(context)
+ val database = Injekt.get()
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val dataSourceFactory = DataSource.Factory {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
@@ -133,17 +138,42 @@ object Helper {
}
dataSource
}
- DownloadManager(
+ val threadPoolSize = Runtime.getRuntime().availableProcessors()
+ val executorService = Executors.newFixedThreadPool(threadPoolSize)
+ val downloadManager = DownloadManager(
context,
database,
- SimpleCache(downloadDirectory, NoOpCacheEvictor(), database),
+ getSimpleCache(context),
dataSourceFactory,
- Executor(Runnable::run)
+ executorService
).apply {
requirements =
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
maxParallelDownloads = 3
}
+ downloadManager.addListener( //for testing
+ object : DownloadManager.Listener {
+ override fun onDownloadChanged(
+ downloadManager: DownloadManager,
+ download: Download,
+ finalException: Exception?
+ ) {
+ if (download.state == Download.STATE_COMPLETED) {
+ Log.e("Downloader", "Download Completed")
+ } else if (download.state == Download.STATE_FAILED) {
+ Log.e("Downloader", "Download Failed")
+ } else if (download.state == Download.STATE_STOPPED) {
+ Log.e("Downloader", "Download Stopped")
+ } else if (download.state == Download.STATE_QUEUED) {
+ Log.e("Downloader", "Download Queued")
+ } else if (download.state == Download.STATE_DOWNLOADING) {
+ Log.e("Downloader", "Download Downloading")
+ }
+ }
+ }
+ )
+
+ downloadManager
}
}
@@ -159,4 +189,108 @@ object Helper {
}
return downloadDirectory!!
}
+
+ @OptIn(UnstableApi::class)
+ fun startAnimeDownloadService(
+ context: Context,
+ title: String,
+ episode: String,
+ video: Video,
+ subtitle: Subtitle? = null,
+ sourceMedia: Media? = null,
+ episodeImage: String? = null
+ ) {
+ if (!isNotificationPermissionGranted(context)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ActivityCompat.requestPermissions(
+ context as Activity,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 1
+ )
+ }
+ }
+
+ val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
+ title,
+ episode,
+ video,
+ subtitle,
+ sourceMedia,
+ episodeImage
+ )
+
+ val downloadsManger = Injekt.get()
+ val downloadCheck = downloadsManger
+ .queryDownload(title, episode, DownloadedType.Type.ANIME)
+
+ if (downloadCheck) {
+ AlertDialog.Builder(context, R.style.MyPopup)
+ .setTitle("Download Exists")
+ .setMessage("A download for this episode already exists. Do you want to overwrite it?")
+ .setPositiveButton("Yes") { _, _ ->
+ DownloadService.sendRemoveDownload(
+ context,
+ ExoplayerDownloadService::class.java,
+ context.getSharedPreferences(
+ getString(context, R.string.anime_downloads),
+ Context.MODE_PRIVATE
+ ).getString(
+ animeDownloadTask.getTaskName(),
+ ""
+ ) ?: "",
+ false
+ )
+ context.getSharedPreferences(
+ getString(context, R.string.anime_downloads),
+ Context.MODE_PRIVATE
+ ).edit()
+ .remove(animeDownloadTask.getTaskName())
+ .apply()
+ downloadsManger.removeDownload(
+ DownloadedType(
+ title,
+ episode,
+ DownloadedType.Type.ANIME
+ )
+ )
+ AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
+ if (!AnimeServiceDataSingleton.isServiceRunning) {
+ val intent = Intent(context, AnimeDownloaderService::class.java)
+ ContextCompat.startForegroundService(context, intent)
+ AnimeServiceDataSingleton.isServiceRunning = true
+ }
+ }
+ .setNegativeButton("No") { _, _ -> }
+ .show()
+ } else {
+ AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
+ if (!AnimeServiceDataSingleton.isServiceRunning) {
+ val intent = Intent(context, AnimeDownloaderService::class.java)
+ ContextCompat.startForegroundService(context, intent)
+ AnimeServiceDataSingleton.isServiceRunning = true
+ }
+ }
+ }
+
+ @OptIn(UnstableApi::class)
+ fun getSimpleCache(context: Context): SimpleCache {
+ return if (simpleCache == null) {
+ val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
+ val database = Injekt.get()
+ simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
+ simpleCache!!
+ } else {
+ simpleCache!!
+ }
+ }
+
+ private fun isNotificationPermissionGranted(context: Context): Boolean {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return ActivityCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+ return true
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
index 1a08934d82c..8d0cd6aba47 100644
--- a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
@@ -47,6 +47,7 @@ import kotlin.math.min
class AnimeFragment : Fragment() {
private var _binding: FragmentAnimeBinding? = null
private val binding get() = _binding!!
+ private lateinit var animePageAdapter: AnimePageAdapter
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
@@ -95,7 +96,7 @@ class AnimeFragment : Fragment() {
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
- val animePageAdapter = AnimePageAdapter()
+ animePageAdapter = AnimePageAdapter()
var loading = true
if (model.notSet) {
@@ -277,6 +278,11 @@ class AnimeFragment : Fragment() {
override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
+ if (animePageAdapter.trendingViewPager != null) {
+ binding.root.requestApplyInsets()
+ binding.root.requestLayout()
+ }
+
super.onResume()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt
index 1f3255ddada..a2ee6593310 100644
--- a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt
@@ -111,7 +111,6 @@ class HomeFragment : Fragment() {
snackString(currContext()?.getString(R.string.please_reload))
}
}
-
binding.homeUserAvatarContainer.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
diff --git a/app/src/main/java/ani/dantotsu/home/LoginFragment.kt b/app/src/main/java/ani/dantotsu/home/LoginFragment.kt
index d3144237311..58bb2fc9055 100644
--- a/app/src/main/java/ani/dantotsu/home/LoginFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/LoginFragment.kt
@@ -28,5 +28,6 @@ class LoginFragment : Fragment() {
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
+ binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
index 14870b7fad5..d4b5095ec9c 100644
--- a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
@@ -43,6 +43,7 @@ import kotlin.math.min
class MangaFragment : Fragment() {
private var _binding: FragmentMangaBinding? = null
private val binding get() = _binding!!
+ private lateinit var mangaPageAdapter: MangaPageAdapter
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
@@ -90,7 +91,7 @@ class MangaFragment : Fragment() {
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
- val mangaPageAdapter = MangaPageAdapter()
+ mangaPageAdapter = MangaPageAdapter()
var loading = true
if (model.notSet) {
model.notSet = false
@@ -251,6 +252,11 @@ class MangaFragment : Fragment() {
override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
+ //make sure mangaPageAdapter is initialized
+ if (mangaPageAdapter.trendingViewPager != null) {
+ binding.root.requestApplyInsets()
+ binding.root.requestLayout()
+ }
super.onResume()
}
diff --git a/app/src/main/java/ani/dantotsu/home/NoInternet.kt b/app/src/main/java/ani/dantotsu/home/NoInternet.kt
index 524aa825477..147cf8874b1 100644
--- a/app/src/main/java/ani/dantotsu/home/NoInternet.kt
+++ b/app/src/main/java/ani/dantotsu/home/NoInternet.kt
@@ -4,8 +4,11 @@ import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.view.View
import android.view.ViewGroup
+import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach
@@ -17,6 +20,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.R
import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.databinding.ActivityNoInternetBinding
+import ani.dantotsu.download.anime.OfflineAnimeFragment
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
@@ -25,6 +29,7 @@ import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.others.LangSet
import ani.dantotsu.selectedOption
import ani.dantotsu.settings.UserInterfaceSettings
+import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
import nl.joery.animatedbottombar.AnimatedBottomBar
@@ -56,10 +61,24 @@ class NoInternet : AppCompatActivity() {
}
+ var doubleBackToExitPressedOnce = false
+ onBackPressedDispatcher.addCallback(this) {
+ if (doubleBackToExitPressedOnce) {
+ finishAffinity()
+ }
+ doubleBackToExitPressedOnce = true
+ snackString(this@NoInternet.getString(R.string.back_to_exit))
+ Handler(Looper.getMainLooper()).postDelayed(
+ { doubleBackToExitPressedOnce = false },
+ 2000
+ )
+ }
+
binding.root.doOnAttach {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab
+
binding.includedNavbar.navbarContainer.updateLayoutParams {
bottomMargin = navBarHeight
}
@@ -96,12 +115,11 @@ class NoInternet : AppCompatActivity() {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
- when (position) {
- 0 -> return OfflineFragment()
- 1 -> return OfflineFragment()
- 2 -> return OfflineMangaFragment()
+ return when (position) {
+ 0 -> OfflineAnimeFragment()
+ 2 -> OfflineMangaFragment()
+ else -> OfflineFragment()
}
- return LoginFragment()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt b/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt
index d6428a38e38..77035416c47 100644
--- a/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt
@@ -4,11 +4,13 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.util.TypedValue
import android.view.View
+import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
+import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
@@ -16,8 +18,10 @@ import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.user.ListViewPagerAdapter
+import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.settings.UserInterfaceSettings
+import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -76,6 +80,10 @@ class CalendarActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
+ binding.settingsContainer.updateLayoutParams {
+ topMargin = statusBarHeight
+ bottomMargin = navBarHeight
+ }
}
setContentView(binding.root)
diff --git a/app/src/main/java/ani/dantotsu/media/Media.kt b/app/src/main/java/ani/dantotsu/media/Media.kt
index 3360a94a673..ebc4d4f7cb9 100644
--- a/app/src/main/java/ani/dantotsu/media/Media.kt
+++ b/app/src/main/java/ani/dantotsu/media/Media.kt
@@ -118,6 +118,19 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
}
+fun emptyMedia() = Media(
+ id = 0,
+ name = "No media found",
+ nameRomaji = "No media found",
+ userPreferredName = "",
+ isAdult = false,
+ isFav = false,
+ isListPrivate = false,
+ userScore = 0,
+ userStatus = "",
+ format = "",
+)
+
object MediaSingleton {
var media: Media? = null
var bitmap: Bitmap? = null
diff --git a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt
index fd04eb58cd7..6d19fdf47fe 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt
@@ -13,7 +13,10 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
+import androidx.core.util.Pair
+import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
@@ -136,7 +139,7 @@ class MediaAdaptor(
val media = mediaList?.get(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
- b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400)
+ b.itemCompactBanner.loadImage(media.banner ?: media.cover)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
@@ -319,6 +322,7 @@ class MediaAdaptor(
itemView.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
+ binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
@@ -332,6 +336,7 @@ class MediaAdaptor(
itemView.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
+ binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
@@ -346,6 +351,7 @@ class MediaAdaptor(
binding.itemCompactImage.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
+ binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
@@ -361,12 +367,14 @@ class MediaAdaptor(
binding.itemCompactImage.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
+ binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
binding.itemCompactTitleContainer.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
+ binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
@@ -375,7 +383,7 @@ class MediaAdaptor(
}
}
- fun clicked(position: Int, bitmap: Bitmap? = null) {
+ fun clicked(position: Int, itemCompactImage: ImageView?, bitmap: Bitmap? = null) {
if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position)
if (bitmap != null) MediaSingleton.bitmap = bitmap
@@ -384,7 +392,13 @@ class MediaAdaptor(
Intent(activity, MediaDetailsActivity::class.java).putExtra(
"media",
media as Serializable
- ), null
+ ), ActivityOptionsCompat.makeSceneTransitionAnimation(
+ activity,
+ Pair.create(
+ itemCompactImage,
+ ViewCompat.getTransitionName(activity.findViewById(R.id.itemCompactImage))!!
+ ),
+ ).toBundle()
)
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
index a902a1cf43d..66c250291f7 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
@@ -2,6 +2,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
+import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.SpannableStringBuilder
@@ -42,7 +43,6 @@ import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog
-import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized
import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings
@@ -72,11 +72,17 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
- LangSet.setLocale(this)
- var media: Media = intent.getSerialized("media") ?: return
+ super.onCreate(savedInstanceState)
+ var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
+ if (media.name == "No media found") {
+ snackString(media.name)
+ onBackPressedDispatcher.onBackPressed()
+ return
+ }
+ mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null
- super.onCreate(savedInstanceState)
+
binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat()
@@ -85,12 +91,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
initActivity(this)
uiSettings = loadData("ui_settings") ?: UserInterfaceSettings()
- if (!uiSettings.immersiveMode) this.window.statusBarColor =
- ContextCompat.getColor(this, R.color.nav_bg_inv)
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams { topMargin += statusBarHeight }
+ binding.incognito.updateLayoutParams { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams {
@@ -154,7 +159,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
})
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
- binding.mediaTitle.text = media.userPreferredName
+ if (this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ .getBoolean("incognito", false)) {
+ binding.mediaTitle.text = " ${media.userPreferredName}"
+ binding.incognito.visibility = View.VISIBLE
+ }else {
+ binding.mediaTitle.text = media.userPreferredName
+ }
binding.mediaTitle.setOnLongClickListener {
copyToClipboard(media.userPreferredName)
true
@@ -305,7 +316,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
adult = media.isAdult
-
tabLayout.menu.clear()
if (media.anime != null) {
viewPager.adapter =
@@ -317,7 +327,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
lifecycle,
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
)
- tabLayout.inflateMenu(R.menu.manga_menu_detail)
+ if (media.format == "NOVEL") {
+ tabLayout.inflateMenu(R.menu.novel_menu_detail)
+ } else {
+ tabLayout.inflateMenu(R.menu.manga_menu_detail)
+ }
anime = false
}
@@ -442,7 +456,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
.setDuration(duration).start()
binding.mediaBanner.pause()
- if (!uiSettings.immersiveMode) this.window.statusBarColor = color
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
@@ -455,7 +468,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
.setDuration(duration).start()
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
- if (!uiSettings.immersiveMode) this.window.statusBarColor = color
}
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
false
@@ -532,5 +544,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
image.alpha = if (disabled) 0.33f else 1f
}
}
+
+ companion object {
+ var mediaSingleton: Media? = null
+ }
}
diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
index 35013e02d50..cb5304726a7 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
@@ -242,7 +242,8 @@ class MediaDetailsViewModel : ViewModel() {
i: String,
manager: FragmentManager,
launch: Boolean = true,
- prevEp: String? = null
+ prevEp: String? = null,
+ isDownload: Boolean = false
) {
Handler(Looper.getMainLooper()).post {
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
@@ -254,13 +255,17 @@ class MediaDetailsViewModel : ViewModel() {
}
media.selected = this.loadSelected(media)
val selector =
- SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
+ SelectorDialogFragment.newInstance(
+ media.selected!!.server,
+ launch,
+ prevEp,
+ isDownload
+ )
selector.show(manager, "dialog")
}
}
}
-
//Manga
var mangaReadSources: MangaReadSources? = null
@@ -314,7 +319,8 @@ class MediaDetailsViewModel : ViewModel() {
val novelSources = NovelSources
val novelResponses = MutableLiveData>(null)
suspend fun searchNovels(query: String, i: Int) {
- val source = novelSources[i]
+ val position = if (i >= novelSources.list.size) 0 else i
+ val source = novelSources[position]
tryWithSuspend(post = true) {
if (source != null) {
novelResponses.postValue(source.search(query))
diff --git a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt
index a7b78f8e5a5..c3c290aa60c 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt
@@ -2,6 +2,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
+import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
@@ -59,6 +60,7 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels()
+ val offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("offlineMode", false) || !isOnline(requireContext())
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.updateLayoutParams { bottomMargin += 128f.px + navBarHeight }
@@ -101,29 +103,33 @@ class MediaInfoFragment : Fragment() {
if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
- binding.mediaInfoStudioContainer.setOnClickListener {
- ContextCompat.startActivity(
- requireActivity(),
- Intent(activity, StudioActivity::class.java).putExtra(
- "studio",
- media.anime.mainStudio!! as Serializable
- ),
- null
- )
+ if (!offline) {
+ binding.mediaInfoStudioContainer.setOnClickListener {
+ ContextCompat.startActivity(
+ requireActivity(),
+ Intent(activity, StudioActivity::class.java).putExtra(
+ "studio",
+ media.anime.mainStudio!! as Serializable
+ ),
+ null
+ )
+ }
}
}
if (media.anime.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.anime.author!!.name
- binding.mediaInfoAuthorContainer.setOnClickListener {
- ContextCompat.startActivity(
- requireActivity(),
- Intent(activity, AuthorActivity::class.java).putExtra(
- "author",
- media.anime.author!! as Serializable
- ),
- null
- )
+ if (!offline) {
+ binding.mediaInfoAuthorContainer.setOnClickListener {
+ ContextCompat.startActivity(
+ requireActivity(),
+ Intent(activity, AuthorActivity::class.java).putExtra(
+ "author",
+ media.anime.author!! as Serializable
+ ),
+ null
+ )
+ }
}
}
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
@@ -137,15 +143,17 @@ class MediaInfoFragment : Fragment() {
if (media.manga.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.manga.author!!.name
- binding.mediaInfoAuthorContainer.setOnClickListener {
- ContextCompat.startActivity(
- requireActivity(),
- Intent(activity, AuthorActivity::class.java).putExtra(
- "author",
- media.manga.author!! as Serializable
- ),
- null
- )
+ if (!offline) {
+ binding.mediaInfoAuthorContainer.setOnClickListener {
+ ContextCompat.startActivity(
+ requireActivity(),
+ Intent(activity, AuthorActivity::class.java).putExtra(
+ "author",
+ media.manga.author!! as Serializable
+ ),
+ null
+ )
+ }
}
}
}
@@ -189,7 +197,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
- if (media.trailer != null) {
+ if (media.trailer != null && !offline) {
@Suppress("DEPRECATION")
class MyChrome : WebChromeClient() {
private var mCustomView: View? = null
@@ -243,7 +251,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
- if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) {
+ if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty()) && !offline) {
val markWon = Markwon.builder(requireContext())
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
@@ -304,7 +312,7 @@ class MediaInfoFragment : Fragment() {
}
}
- if (media.genres.isNotEmpty()) {
+ if (media.genres.isNotEmpty() && !offline) {
val bind = ActivityGenreBinding.inflate(
LayoutInflater.from(context),
parent,
@@ -335,7 +343,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
- if (media.tags.isNotEmpty()) {
+ if (media.tags.isNotEmpty() && !offline) {
val bind = ItemTitleChipgroupBinding.inflate(
LayoutInflater.from(context),
parent,
@@ -376,7 +384,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
- if (!media.characters.isNullOrEmpty()) {
+ if (!media.characters.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
@@ -393,7 +401,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
- if (!media.relations.isNullOrEmpty()) {
+ if (!media.relations.isNullOrEmpty() && !offline) {
if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate(
LayoutInflater.from(context),
@@ -456,7 +464,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bindi.root)
}
- if (!media.recommendations.isNullOrEmpty()) {
+ if (!media.recommendations.isNullOrEmpty() && !offline ) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
diff --git a/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt
index b1576e40ab5..fe5c0c93a04 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt
@@ -16,7 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListBinding
-import com.google.android.material.switchmaterial.SwitchMaterial
+import com.google.android.material.materialswitch.MaterialSwitch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -196,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
}
media?.inCustomListsOf?.forEach {
- SwitchMaterial(requireContext()).apply {
+ MaterialSwitch(requireContext()).apply {
isChecked = it.value
text = it.key
setOnCheckedChangeListener { _, isChecked ->
diff --git a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt
index b3cf36f543f..4c71664b5d1 100644
--- a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt
@@ -78,7 +78,7 @@ class SearchActivity : AppCompatActivity() {
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
val headerAdaptor = SearchAdapter(this)
- val gridSize = (screenWidth / 124f).toInt()
+ val gridSize = (screenWidth / 120f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
@@ -159,7 +159,7 @@ class SearchActivity : AppCompatActivity() {
fun search() {
val size = model.searchResults.results.size
model.searchResults.results.clear()
- runOnUiThread {
+ binding.searchRecyclerView.post {
mediaAdaptor.notifyItemRangeRemoved(0, size)
}
diff --git a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt
index 67c523e6784..18fe0a4c2c9 100644
--- a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt
@@ -1,6 +1,8 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
@@ -10,10 +12,14 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
+import ani.dantotsu.App.Companion.context
+import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
+import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.saveData
@@ -54,6 +60,14 @@ class SearchAdapter(private val activity: SearchActivity) :
}
binding.searchBar.hint = activity.result.type
+ if (currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getBoolean("incognito", false ) == true){
+ val startIconDrawableRes = R.drawable.ic_incognito_24
+ val startIconDrawable: Drawable? =
+ context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
+ binding.searchBar.startIconDrawable = startIconDrawable
+ }
+
var adult = activity.result.isAdult
var listOnly = activity.result.onList
diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
index 17759141cb8..d74a89294d6 100644
--- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
+++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
@@ -1,19 +1,23 @@
package ani.dantotsu.media
import android.content.Context
+import ani.dantotsu.download.DownloadedType
+import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType
+import ani.dantotsu.snackString
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
+import java.io.File
class SubtitleDownloader {
companion object {
//doesn't really download the subtitles -\_(o_o)_/-
- suspend fun downloadSubtitles(context: Context, url: String): SubtitleType =
+ suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
withContext(Dispatchers.IO) {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val networkHelper = Injekt.get()
@@ -29,8 +33,8 @@ class SubtitleDownloader {
val subtitleType = when {
- responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS
- responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT
+ responseBody.contains("[Script Info]") -> SubtitleType.ASS
+ responseBody.contains("WEBVTT") -> SubtitleType.VTT
else -> SubtitleType.SRT
}
@@ -39,5 +43,41 @@ class SubtitleDownloader {
return@withContext SubtitleType.UNKNOWN
}
}
+
+ //actually downloads lol
+ suspend fun downloadSubtitle(context: Context, url: String, downloadedType: DownloadedType) {
+ try {
+ val directory = DownloadsManager.getDirectory(context, downloadedType.type, downloadedType.title, downloadedType.chapter)
+ if (!directory.exists()) { //just in case
+ directory.mkdirs()
+ }
+ val type = loadSubtitleType(context, url)
+ val subtiteFile = File(directory, "subtitle.${type}")
+ if (subtiteFile.exists()) {
+ subtiteFile.delete()
+ }
+ subtiteFile.createNewFile()
+
+ val client = Injekt.get().client
+ val request = Request.Builder().url(url).build()
+ val reponse = client.newCall(request).execute()
+
+ if (!reponse.isSuccessful) {
+ snackString("Failed to download subtitle")
+ return
+ }
+
+ reponse.body.byteStream().use { input ->
+ subtiteFile.outputStream().use { output ->
+ input.copyTo(output)
+ }
+ }
+ } catch (e: Exception) {
+ snackString("Failed to download subtitle")
+ e.printStackTrace()
+ return
+ }
+
+ }
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt
index 17d20bc6770..f072a549b51 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt
@@ -5,8 +5,13 @@ import java.util.regex.Pattern
class AnimeNameAdapter {
companion object {
+ const val episodeRegex =
+ "(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
+ const val failedEpisodeNumberRegex =
+ "(?
+ mr.value.replaceFirst(mr.groupValues[1], "")
+ }
+ } else {
+ removedNumber
+ }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt
index ea30fa58685..2f70a2a9c85 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt
@@ -1,32 +1,41 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
+import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
-import android.widget.ImageView
+import android.widget.ImageButton
import android.widget.LinearLayout
+import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
+import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment
+import ani.dantotsu.others.LanguageMapper
+import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
+import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
+import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
+
class AnimeWatchAdapter(
private val media: Media,
private val fragment: AnimeWatchFragment,
@@ -41,6 +50,9 @@ class AnimeWatchAdapter(
return ViewHolder(bind)
}
+ private var nestedDialog: AlertDialog? = null
+
+
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
@@ -78,6 +90,17 @@ class AnimeWatchAdapter(
null
)
}
+ val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
+ "Dantotsu",
+ Context.MODE_PRIVATE
+ )
+ ?.getBoolean("offlineMode", false) == true
+ ) View.GONE else View.VISIBLE
+
+ binding.animeSourceNameContainer.visibility = offline
+ binding.animeSourceSettings.visibility = offline
+ binding.animeSourceSearch.visibility = offline
+ binding.animeSourceTitle.visibility = offline
//Source Selection
var source =
@@ -147,8 +170,9 @@ class AnimeWatchAdapter(
}
}
+ //Icons
- //Subscription
+ //subscribe
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
binding.animeSourceSubscribe,
@@ -167,44 +191,99 @@ class AnimeWatchAdapter(
openSettings(fragment.requireContext(), getChannelId(true, media.id))
}
- //Icons
- var reversed = media.selected!!.recyclerReversed
- var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
- binding.animeSourceTop.rotation = if (reversed) -90f else 90f
- binding.animeSourceTop.setOnClickListener {
- reversed = !reversed
- binding.animeSourceTop.rotation = if (reversed) -90f else 90f
- fragment.onIconPressed(style, reversed)
- }
- var selected = when (style) {
- 0 -> binding.animeSourceList
- 1 -> binding.animeSourceGrid
- 2 -> binding.animeSourceCompact
- else -> binding.animeSourceList
- }
- selected.alpha = 1f
- fun selected(it: ImageView) {
- selected.alpha = 0.33f
- selected = it
+ //Nested Button
+ binding.animeNestedButton.setOnClickListener {
+ val dialogView =
+ LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
+ val dialogBinding = DialogLayoutBinding.bind(dialogView)
+ var refresh = false
+ var run = false
+ var reversed = media.selected!!.recyclerReversed
+ var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
+ dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
+ dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
+ dialogBinding.animeSourceTop.setOnClickListener {
+ reversed = !reversed
+ dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
+ dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
+ run = true
+ }
+ //Grids
+ var selected = when (style) {
+ 0 -> dialogBinding.animeSourceList
+ 1 -> dialogBinding.animeSourceGrid
+ 2 -> dialogBinding.animeSourceCompact
+ else -> dialogBinding.animeSourceList
+ }
+ when (style) {
+ 0 -> dialogBinding.layoutText.text = "List"
+ 1 -> dialogBinding.layoutText.text = "Grid"
+ 2 -> dialogBinding.layoutText.text = "Compact"
+ else -> dialogBinding.animeSourceList
+ }
selected.alpha = 1f
+ fun selected(it: ImageButton) {
+ selected.alpha = 0.33f
+ selected = it
+ selected.alpha = 1f
+ }
+ dialogBinding.animeSourceList.setOnClickListener {
+ selected(it as ImageButton)
+ style = 0
+ dialogBinding.layoutText.text = "List"
+ run = true
+ }
+ dialogBinding.animeSourceGrid.setOnClickListener {
+ selected(it as ImageButton)
+ style = 1
+ dialogBinding.layoutText.text = "Grid"
+ run = true
+ }
+ dialogBinding.animeSourceCompact.setOnClickListener {
+ selected(it as ImageButton)
+ style = 2
+ dialogBinding.layoutText.text = "Compact"
+ run = true
+ }
+ dialogBinding.animeWebviewContainer.setOnClickListener {
+ if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
+ toast("WebView not installed")
+ }
+ //start CookieCatcher activity
+ if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
+ val sourceAHH = watchSources[source] as? DynamicAnimeParser
+ val sourceHttp =
+ sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
+ val url = sourceHttp?.baseUrl
+ url?.let {
+ refresh = true
+ val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
+ .putExtra("url", url)
+ startActivity(fragment.requireContext(), intent, null)
+ }
+ }
+ }
+
+ //hidden
+ dialogBinding.animeScanlatorContainer.visibility = View.GONE
+ dialogBinding.animeDownloadContainer.visibility = View.GONE
+
+ nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
+ .setTitle("Options")
+ .setView(dialogView)
+ .setPositiveButton("OK") { _, _ ->
+ if (run) fragment.onIconPressed(style, reversed)
+ if (refresh) fragment.loadEpisodes(source, true)
+ }
+ .setNegativeButton("Cancel") { _, _ ->
+ if (refresh) fragment.loadEpisodes(source, true)
+ }
+ .setOnCancelListener {
+ if (refresh) fragment.loadEpisodes(source, true)
+ }
+ .create()
+ nestedDialog?.show()
}
- binding.animeSourceList.setOnClickListener {
- selected(it as ImageView)
- style = 0
- fragment.onIconPressed(style, reversed)
- }
- binding.animeSourceGrid.setOnClickListener {
- selected(it as ImageView)
- style = 1
- fragment.onIconPressed(style, reversed)
- }
- binding.animeSourceCompact.setOnClickListener {
- selected(it as ImageView)
- style = 2
- fragment.onIconPressed(style, reversed)
- }
- binding.animeScanlatorTop.visibility = View.GONE
- binding.animeDownloadTop.visibility = View.GONE
//Episode Handling
handleEpisodes()
}
@@ -304,12 +383,15 @@ class AnimeWatchAdapter(
}
}
val ep = media.anime.episodes!![continueEp]!!
+
+ val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
+
binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text =
- currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
+ currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp)
}
@@ -322,6 +404,7 @@ class AnimeWatchAdapter(
} else {
binding.animeSourceContinue.visibility = View.GONE
}
+
binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty())
binding.animeSourceNotFound.visibility = View.GONE
@@ -336,7 +419,7 @@ class AnimeWatchAdapter(
}
}
- fun setLanguageList(lang: Int, source: Int) {
+ private fun setLanguageList(lang: Int, source: Int) {
val binding = _binding
if (watchSources is AnimeSources) {
val parser = watchSources[source] as? DynamicAnimeParser
@@ -351,12 +434,16 @@ class AnimeWatchAdapter(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
}
- binding?.animeSourceLanguage?.setAdapter(
- ArrayAdapter(
- fragment.requireContext(),
- R.layout.item_dropdown,
- parser.extension.sources.map { it.lang })
+ val adapter = ArrayAdapter(
+ fragment.requireContext(),
+ R.layout.item_dropdown,
+ parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
)
+ val items = adapter.count
+
+ binding?.animeSourceLanguageContainer?.visibility =
+ if (items > 1) View.VISIBLE else View.GONE
+ binding?.animeSourceLanguage?.setAdapter(adapter)
}
}
@@ -371,4 +458,4 @@ class AnimeWatchAdapter(
countDown(media, binding.animeSourceContainer)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt
index df3522200a8..487bf207185 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt
@@ -2,6 +2,10 @@ package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.AlertDialog
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@@ -9,20 +13,29 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
+import androidx.annotation.OptIn
import androidx.cardview.widget.CardView
+import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
+import ani.dantotsu.download.DownloadedType
+import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.anime.AnimeDownloaderService
+import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
+import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
@@ -42,6 +55,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.roundToInt
@@ -61,6 +76,8 @@ class AnimeWatchFragment : Fragment() {
private lateinit var headerAdapter: AnimeWatchAdapter
private lateinit var episodeAdapter: EpisodeAdapter
+ val downloadManager = Injekt.get()
+
var screenWidth = 0f
private var progress = View.VISIBLE
@@ -80,6 +97,21 @@ class AnimeWatchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ val intentFilter = IntentFilter().apply {
+ addAction(ACTION_DOWNLOAD_STARTED)
+ addAction(ACTION_DOWNLOAD_FINISHED)
+ addAction(ACTION_DOWNLOAD_FAILED)
+ addAction(ACTION_DOWNLOAD_PROGRESS)
+ }
+
+ ContextCompat.registerReceiver(
+ requireContext(),
+ downloadStatusReceiver,
+ intentFilter,
+ ContextCompat.RECEIVER_EXPORTED
+ )
+
+
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp
@@ -134,9 +166,17 @@ class AnimeWatchFragment : Fragment() {
if (!loaded) {
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
+ val offlineMode =
+ model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
+
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
episodeAdapter =
- EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
+ EpisodeAdapter(
+ style ?: uiSettings.animeDefaultView,
+ media,
+ this,
+ offlineMode = offlineMode
+ )
binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, episodeAdapter)
@@ -169,12 +209,15 @@ class AnimeWatchFragment : Fragment() {
if (media.anime?.kitsuEpisodes != null) {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc =
- episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
- episode.title =
- episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
- episode.thumb =
- episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb
- ?: FileUrl[media.cover]
+ media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
+ episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
+ episode.title ?: ""
+ ).isBlank()
+ ) media.anime!!.kitsuEpisodes!![i]?.title
+ ?: episode.title else episode.title
+ ?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
+ episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
+ ?: FileUrl[media.cover]
}
}
}
@@ -314,19 +357,20 @@ class AnimeWatchFragment : Fragment() {
if (show) View.GONE else View.VISIBLE
}
}
+ var itemSelected = false
val allSettings = pkg.sources.filterIsInstance()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
- val names = allSettings.map { it.lang }.toTypedArray()
+ val names =
+ allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
var selectedIndex = 0
- val dialog = AlertDialog.Builder(requireContext())
+ val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
- .setSingleChoiceItems(names, selectedIndex) { _, which ->
+ .setSingleChoiceItems(names, selectedIndex) { dialog, which ->
selectedIndex = which
- }
- .setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
+ itemSelected = true
dialog.dismiss()
// Move the fragment transaction here
@@ -343,10 +387,10 @@ class AnimeWatchFragment : Fragment() {
.commit()
}
}
- .setNegativeButton("Cancel") { dialog, _ ->
- dialog.cancel()
- changeUIVisibility(true)
- return@setNegativeButton
+ .setOnDismissListener {
+ if (!itemSelected) {
+ changeUIVisibility(true)
+ }
}
.show()
dialog.window?.setDimAmount(0.8f)
@@ -379,6 +423,93 @@ class AnimeWatchFragment : Fragment() {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
}
+ fun onAnimeEpisodeDownloadClick(i: String) {
+ model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
+ }
+
+ fun onAnimeEpisodeStopDownloadClick(i: String) {
+ val cancelIntent = Intent().apply {
+ action = AnimeDownloaderService.ACTION_CANCEL_DOWNLOAD
+ putExtra(
+ AnimeDownloaderService.EXTRA_TASK_NAME,
+ AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
+ )
+ }
+ requireContext().sendBroadcast(cancelIntent)
+
+ // Remove the download from the manager and update the UI
+ downloadManager.removeDownload(
+ DownloadedType(
+ media.mainName(),
+ i,
+ DownloadedType.Type.ANIME
+ )
+ )
+ episodeAdapter.purgeDownload(i)
+ }
+
+ @OptIn(UnstableApi::class)
+ fun onAnimeEpisodeRemoveDownloadClick(i: String) {
+ downloadManager.removeDownload(
+ DownloadedType(
+ media.mainName(),
+ i,
+ DownloadedType.Type.ANIME
+ )
+ )
+ val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
+ val id = requireContext().getSharedPreferences(
+ ContextCompat.getString(requireContext(), R.string.anime_downloads),
+ Context.MODE_PRIVATE
+ ).getString(
+ taskName,
+ ""
+ ) ?: ""
+ requireContext().getSharedPreferences(
+ ContextCompat.getString(requireContext(), R.string.anime_downloads),
+ Context.MODE_PRIVATE
+ ).edit().remove(taskName).apply()
+ DownloadService.sendRemoveDownload(
+ requireContext(),
+ ExoplayerDownloadService::class.java,
+ id,
+ true
+ )
+ episodeAdapter.deleteDownload(i)
+ }
+
+ private val downloadStatusReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (!this@AnimeWatchFragment::episodeAdapter.isInitialized) return
+ when (intent.action) {
+ ACTION_DOWNLOAD_STARTED -> {
+ val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
+ chapterNumber?.let { episodeAdapter.startDownload(it) }
+ }
+
+ ACTION_DOWNLOAD_FINISHED -> {
+ val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
+ chapterNumber?.let { episodeAdapter.stopDownload(it) }
+ }
+
+ ACTION_DOWNLOAD_FAILED -> {
+ val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
+ chapterNumber?.let {
+ episodeAdapter.purgeDownload(it)
+ }
+ }
+
+ ACTION_DOWNLOAD_PROGRESS -> {
+ val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
+ val progress = intent.getIntExtra("progress", 0)
+ chapterNumber?.let {
+ episodeAdapter.updateDownloadProgress(it, progress)
+ }
+ }
+ }
+ }
+ }
+
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
@@ -391,6 +522,8 @@ class AnimeWatchFragment : Fragment() {
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes()
+ val isDownloaded = model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
+ episodeAdapter.offlineMode = isDownloaded
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList = arrayListOf()
if (media.anime!!.episodes != null) {
@@ -405,11 +538,20 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
+ for (download in downloadManager.animeDownloadedTypes) {
+ if (download.title == media.mainName()) {
+ episodeAdapter.stopDownload(download.chapter)
+ }
+ }
}
override fun onDestroy() {
model.watchSources?.flushText()
super.onDestroy()
+ try {
+ requireContext().unregisterReceiver(downloadStatusReceiver)
+ } catch (_: IllegalArgumentException) {
+ }
}
var state: Parcelable? = null
@@ -424,4 +566,12 @@ class AnimeWatchFragment : Fragment() {
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
}
-}
\ No newline at end of file
+ companion object {
+ const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
+ const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
+ const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
+ const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
+ const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
+ }
+
+}
diff --git a/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt b/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt
new file mode 100644
index 00000000000..611f378a34d
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt
@@ -0,0 +1,43 @@
+package ani.dantotsu.media.anime
+
+import android.content.Context
+import android.os.Bundle
+import androidx.mediarouter.app.MediaRouteActionProvider
+import androidx.mediarouter.app.MediaRouteChooserDialog
+import androidx.mediarouter.app.MediaRouteChooserDialogFragment
+import androidx.mediarouter.app.MediaRouteControllerDialog
+import androidx.mediarouter.app.MediaRouteControllerDialogFragment
+import androidx.mediarouter.app.MediaRouteDialogFactory
+import ani.dantotsu.R
+
+class CustomCastProvider(context: Context) : MediaRouteActionProvider(context) {
+ init {
+ dialogFactory = CustomCastThemeFactory()
+ }
+}
+
+class CustomCastThemeFactory : MediaRouteDialogFactory() {
+ override fun onCreateChooserDialogFragment(): MediaRouteChooserDialogFragment {
+ return CustomMediaRouterChooserDialogFragment()
+ }
+
+ override fun onCreateControllerDialogFragment(): MediaRouteControllerDialogFragment {
+ return CustomMediaRouteControllerDialogFragment()
+ }
+}
+
+class CustomMediaRouterChooserDialogFragment : MediaRouteChooserDialogFragment() {
+ override fun onCreateChooserDialog(
+ context: Context,
+ savedInstanceState: Bundle?
+ ): MediaRouteChooserDialog =
+ MediaRouteChooserDialog(context, R.style.MyPopup)
+}
+
+class CustomMediaRouteControllerDialogFragment : MediaRouteControllerDialogFragment() {
+ override fun onCreateControllerDialog(
+ context: Context,
+ savedInstanceState: Bundle?
+ ): MediaRouteControllerDialog =
+ MediaRouteControllerDialog(context, R.style.MyPopup)
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/media/anime/Episode.kt b/app/src/main/java/ani/dantotsu/media/anime/Episode.kt
index 1461524648e..535174cd475 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/Episode.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/Episode.kt
@@ -14,7 +14,8 @@ data class Episode(
var selectedExtractor: String? = null,
var selectedVideo: Int = 0,
var selectedSubtitle: Int? = -1,
- var extractors: MutableList? = null,
+ var downloadProgress: String? = null,
+ @Transient var extractors: MutableList? = null,
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
var allStreams: Boolean = false,
var watched: Long? = null,
diff --git a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt
index 1a85b9bf19b..54e851edb1c 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt
@@ -1,19 +1,32 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
+import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.LinearInterpolator
import android.widget.LinearLayout
+import androidx.annotation.OptIn
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.coroutineScope
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.offline.DownloadIndex
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding
+import ani.dantotsu.download.anime.AnimeDownloaderService
+import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.math.ln
+import kotlin.math.pow
fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
val curr = loadData("${mediaId}_${ep}")
@@ -32,13 +45,24 @@ fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep:
}
}
+@OptIn(UnstableApi::class)
class EpisodeAdapter(
private var type: Int,
private val media: Media,
private val fragment: AnimeWatchFragment,
- var arr: List = arrayListOf()
+ var arr: List = arrayListOf(),
+ var offlineMode: Boolean
) : RecyclerView.Adapter() {
+ private lateinit var index: DownloadIndex
+
+
+ init {
+ if (offlineMode) {
+ index = Helper.downloadManager(fragment.requireContext()).downloadIndex
+ }
+ }
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) {
0 -> EpisodeListViewHolder(
@@ -76,12 +100,11 @@ class EpisodeAdapter(
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position]
- val title =
- "${
- if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(
- R.string.episode_singular
- )
- } ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
+ val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
+ ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
+ } else {
+ ep.number
+ } ?: ""
when (holder) {
is EpisodeListViewHolder -> {
@@ -93,7 +116,7 @@ class EpisodeAdapter(
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number
- binding.itemEpisodeTitle.text = title
+ binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
if (ep.filler) {
binding.itemEpisodeFiller.visibility = View.VISIBLE
@@ -102,6 +125,7 @@ class EpisodeAdapter(
binding.itemEpisodeFiller.visibility = View.GONE
binding.itemEpisodeFillerView.visibility = View.GONE
}
+ holder.bind(ep.number, ep.downloadProgress)
binding.itemEpisodeDesc.visibility =
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = ep.desc ?: ""
@@ -205,6 +229,80 @@ class EpisodeAdapter(
override fun getItemCount(): Int = arr.size
+ private val activeDownloads = mutableSetOf()
+ private val downloadedEpisodes = mutableSetOf()
+
+ fun startDownload(episodeNumber: String) {
+ activeDownloads.add(episodeNumber)
+ // Find the position of the chapter and notify only that item
+ val position = arr.indexOfFirst { it.number == episodeNumber }
+ if (position != -1) {
+ notifyItemChanged(position)
+ }
+ }
+
+ @OptIn(UnstableApi::class)
+ fun stopDownload(episodeNumber: String) {
+ activeDownloads.remove(episodeNumber)
+ downloadedEpisodes.add(episodeNumber)
+ // Find the position of the chapter and notify only that item
+ val position = arr.indexOfFirst { it.number == episodeNumber }
+ if (position != -1) {
+ val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
+ media.mainName(),
+ episodeNumber
+ )
+ val id = fragment.requireContext().getSharedPreferences(
+ ContextCompat.getString(fragment.requireContext(), R.string.anime_downloads),
+ Context.MODE_PRIVATE
+ ).getString(
+ taskName,
+ ""
+ ) ?: ""
+ val size = try {
+ val download = index.getDownload(id)
+ bytesToHuman(download?.bytesDownloaded ?: 0)
+ } catch (e: Exception) {
+ null
+ }
+
+ arr[position].downloadProgress = "Downloaded" + if (size != null) ": ($size)" else ""
+ notifyItemChanged(position)
+ }
+ }
+
+ fun deleteDownload(episodeNumber: String) {
+ downloadedEpisodes.remove(episodeNumber)
+ // Find the position of the chapter and notify only that item
+ val position = arr.indexOfFirst { it.number == episodeNumber }
+ if (position != -1) {
+ arr[position].downloadProgress = null
+ notifyItemChanged(position)
+ }
+ }
+
+ fun purgeDownload(episodeNumber: String) {
+ activeDownloads.remove(episodeNumber)
+ downloadedEpisodes.remove(episodeNumber)
+ // Find the position of the chapter and notify only that item
+ val position = arr.indexOfFirst { it.number == episodeNumber }
+ if (position != -1) {
+ arr[position].downloadProgress = "Failed"
+ notifyItemChanged(position)
+ }
+ }
+
+ fun updateDownloadProgress(episodeNumber: String, progress: Int) {
+ // Find the position of the chapter and notify only that item
+ val position = arr.indexOfFirst { it.number == episodeNumber }
+ if (position != -1) {
+ arr[position].downloadProgress = "Downloading: $progress%"
+
+ notifyItemChanged(position)
+ }
+ }
+
+
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
@@ -227,11 +325,27 @@ class EpisodeAdapter(
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
RecyclerView.ViewHolder(binding.root) {
+ private val activeCoroutines = mutableSetOf()
+
init {
itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
}
+ binding.itemDownload.setOnClickListener {
+ if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
+ val episodeNumber = arr[bindingAdapterPosition].number
+ if (activeDownloads.contains(episodeNumber)) {
+ fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
+ return@setOnClickListener
+ } else if (downloadedEpisodes.contains(episodeNumber)) {
+ fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
+ return@setOnClickListener
+ } else {
+ fragment.onAnimeEpisodeDownloadClick(episodeNumber)
+ }
+ }
+ }
binding.itemEpisodeDesc.setOnClickListener {
if (binding.itemEpisodeDesc.maxLines == 3)
binding.itemEpisodeDesc.maxLines = 100
@@ -239,11 +353,73 @@ class EpisodeAdapter(
binding.itemEpisodeDesc.maxLines = 3
}
}
+
+ fun bind(episodeNumber: String, progress: String?) {
+ if (progress != null) {
+ binding.itemDownloadStatus.visibility = View.VISIBLE
+ binding.itemDownloadStatus.text = progress
+ } else {
+ binding.itemDownloadStatus.visibility = View.GONE
+ binding.itemDownloadStatus.text = ""
+ }
+ if (activeDownloads.contains(episodeNumber)) {
+ // Show spinner
+ binding.itemDownload.setImageResource(R.drawable.ic_sync)
+ startOrContinueRotation(episodeNumber)
+ } else if (downloadedEpisodes.contains(episodeNumber)) {
+ binding.itemDownloadStatus.visibility = View.VISIBLE
+ // Show checkmark
+ binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
+ //binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places
+ binding.itemDownload.postDelayed({
+ binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
+ binding.itemDownload.rotation = 0f
+ //binding.itemDownload.setColorFilter(typedValue2.data)
+ }, 1000)
+ } else {
+ binding.itemDownloadStatus.visibility = View.GONE
+ // Show download icon
+ binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
+ binding.itemDownload.rotation = 0f
+ }
+
+ }
+
+ private fun startOrContinueRotation(episodeNumber: String) {
+ if (!isRotationCoroutineRunningFor(episodeNumber)) {
+ val scope = fragment.lifecycle.coroutineScope
+ scope.launch {
+ // Add chapter number to active coroutines set
+ activeCoroutines.add(episodeNumber)
+ while (activeDownloads.contains(episodeNumber)) {
+ binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
+ .setInterpolator(
+ LinearInterpolator()
+ ).start()
+ delay(1000)
+ }
+ // Remove chapter number from active coroutines set
+ activeCoroutines.remove(episodeNumber)
+ }
+ }
+ }
+
+ private fun isRotationCoroutineRunningFor(episodeNumber: String): Boolean {
+ return episodeNumber in activeCoroutines
+ }
}
fun updateType(t: Int) {
type = t
}
-}
+ private fun bytesToHuman(bytes: Long): String? {
+ if (bytes < 0) return null
+ val unit = 1000
+ if (bytes < unit) return "$bytes B"
+ val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
+ val pre = ("KMGTPE")[exp - 1]
+ return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
+ }
+}
diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
index f16f47efb20..ee5baedff82 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
@@ -14,7 +14,6 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.Animatable
-import android.hardware.Sensor
import android.hardware.SensorManager
import android.media.AudioManager
import android.media.AudioManager.*
@@ -43,11 +42,15 @@ import androidx.core.math.MathUtils.clamp
import androidx.core.view.WindowCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
+import androidx.media3.cast.CastPlayer
+import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.*
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
import androidx.media3.common.C.TRACK_TYPE_VIDEO
import androidx.media3.common.util.UnstableApi
+import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource
+import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
@@ -58,6 +61,7 @@ import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaSession
import androidx.media3.ui.*
import androidx.media3.ui.CaptionStyleCompat.*
+import androidx.mediarouter.app.MediaRouteButton
import ani.dantotsu.*
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
@@ -67,12 +71,12 @@ import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding
+import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.AniSkip.getType
-import ani.dantotsu.others.Download.download
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.ResettableTimer
import ani.dantotsu.others.getSerialized
@@ -82,6 +86,10 @@ import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager
import com.bumptech.glide.Glide
+import com.google.android.gms.cast.framework.CastButtonFactory
+import com.google.android.gms.cast.framework.CastContext
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.slider.Slider
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.lagradost.nicehttp.ignoreAllSSLErrors
@@ -97,10 +105,9 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
-
@UnstableApi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
-class ExoplayerView : AppCompatActivity(), Player.Listener {
+class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityListener {
private val resumeWindow = "resumeWindow"
private val resumePosition = "resumePosition"
@@ -108,6 +115,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
private val playerOnPlay = "playerOnPlay"
private lateinit var exoPlayer: ExoPlayer
+ private var castPlayer: CastPlayer? = null
+ private var castContext: CastContext? = null
+ private var isCastApiAvailable = false
private lateinit var trackSelector: DefaultTrackSelector
private lateinit var cacheFactory: CacheDataSource.Factory
private lateinit var playbackParameters: PlaybackParameters
@@ -145,6 +155,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
private var orientationListener: OrientationEventListener? = null
+ private var downloadId: String? = null
+
companion object {
var initialized = false
lateinit var media: Media
@@ -328,6 +340,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
setContentView(binding.root)
//Initialize
+ isCastApiAvailable = GoogleApiAvailability.getInstance()
+ .isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
+ try {
+ castContext = CastContext.getSharedInstance(this)
+ castPlayer = CastPlayer(castContext!!)
+ castPlayer!!.setSessionAvailabilityListener(this)
+ } catch (e: Exception) {
+ isCastApiAvailable = false
+ }
+
WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBars()
@@ -387,7 +409,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
- println(orientation)
if (orientation in 45..135) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility =
View.VISIBLE
@@ -466,12 +487,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (isInitialized) {
isPlayerPlaying = exoPlayer.isPlaying
(exoPlay.drawable as Animatable?)?.start()
- if (isPlayerPlaying) {
+ if (isPlayerPlaying || castPlayer?.isPlaying == true) {
Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay)
exoPlayer.pause()
+ castPlayer?.pause()
} else {
- Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
- exoPlayer.play()
+ if (castPlayer?.isPlaying == false && castPlayer?.currentMediaItem != null) {
+ Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
+ castPlayer?.play()
+ } else if (!isPlayerPlaying) {
+ Glide.with(this).load(R.drawable.anim_pause_to_play).into(exoPlay)
+ exoPlayer.play()
+ }
}
}
}
@@ -812,18 +839,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
audioManager.setStreamVolume(STREAM_MUSIC, volume, 0)
volumeHide()
}
-
+ val fastForward = playerView.findViewById(R.id.exo_fast_forward_text)
fun fastForward() {
isFastForwarding = true
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
- snackString("Playing at ${exoPlayer.playbackParameters.speed}x speed")
+ fastForward.visibility = View.VISIBLE
+ fastForward.text = ("${exoPlayer.playbackParameters.speed}x")
}
fun stopFastForward() {
if (isFastForwarding) {
isFastForwarding = false
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2)
- snackString("Playing at default speed: ${exoPlayer.playbackParameters.speed}x")
+ fastForward.visibility = View.GONE
}
}
@@ -925,10 +953,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
episodeArr = episodes.keys.toList()
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
- episodeTitleArr = arrayListOf()
+ episodeTitleArr = arrayListOf()
episodes.forEach {
val episode = it.value
- episodeTitleArr.add("${if (!episode.title.isNullOrEmpty() && episode.title != "null") "" else "Episode "}${episode.number}${if (episode.filler) " [Filler]" else ""}${if (!episode.title.isNullOrEmpty() && episode.title != "null") " : " + episode.title else ""}")
+ val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
+ episodeTitleArr.add("Episode ${episode.number}${if (episode.filler) " [Filler]" else ""}${if (cleanedTitle.isNotBlank() && cleanedTitle != "null") ": $cleanedTitle" else ""}")
}
//Episode Change
@@ -1074,10 +1103,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
//Cast
if (settings.cast) {
- playerView.findViewById(R.id.exo_cast).apply {
+ playerView.findViewById(R.id.exo_cast).apply {
visibility = View.VISIBLE
- setSafeOnClickListener {
+ try {
+ CastButtonFactory.setUpMediaRouteButton(context, this)
+ dialogFactory = CustomCastThemeFactory()
+ } catch (e: Exception) {
+ isCastApiAvailable = false
+ }
+ setOnLongClickListener {
cast()
+ true
}
}
}
@@ -1101,7 +1137,21 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (settings.cursedSpeeds)
arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
else
- arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f)
+ arrayOf(
+ 0.25f,
+ 0.33f,
+ 0.5f,
+ 0.66f,
+ 0.75f,
+ 1f,
+ 1.15f,
+ 1.25f,
+ 1.33f,
+ 1.5f,
+ 1.66f,
+ 1.75f,
+ 2f
+ )
val speedsName = speeds.map { "${it}x" }.toTypedArray()
var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed
@@ -1156,14 +1206,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
preloading = false
+ val incognito = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getBoolean("incognito", false) ?: false
val showProgressDialog =
if (settings.askIndividual) loadData("${media.id}_progressDialog")
?: true else false
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.auto_update, media.userPreferredName))
- .setMessage(getString(R.string.incognito_will_not_update))
.apply {
+ if (incognito) {
+ setMessage(getString(R.string.incognito_will_not_update))
+ }
setOnCancelListener { hideSystemBars() }
setCancelable(false)
setPositiveButton(getString(R.string.yes)) { dialog, _ ->
@@ -1232,7 +1286,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (subtitle?.type == SubtitleType.UNKNOWN) {
val context = this
runBlocking {
- val type = SubtitleDownloader.downloadSubtitles(context, subtitle!!.file.url)
+ val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url)
val fileUri = Uri.parse(subtitle!!.file.url)
sub = MediaItem.SubtitleConfiguration
.Builder(fileUri)
@@ -1250,8 +1304,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
println("sub: $sub")
} else {
+ val subUri = Uri.parse((subtitle!!.file.url))
sub = MediaItem.SubtitleConfiguration
- .Builder(Uri.parse(subtitle!!.file.url))
+ .Builder(subUri)
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType(
when (subtitle?.type) {
@@ -1270,18 +1325,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
ext.onVideoPlayed(video)
}
- val but = playerView.findViewById(R.id.exo_download)
- if (video?.format == VideoType.CONTAINER || (loadData("settings_download_manager")
- ?: 0) != 0
- ) {
- but.visibility = View.VISIBLE
- but.setOnClickListener {
- download(this, episode, animeTitle.text.toString())
- }
- } else {
- but.visibility = View.GONE
- }
-
val simpleCache = VideoCache.getInstance(this)
val httpClient = okHttpClient.newBuilder().apply {
ignoreAllSSLErrors()
@@ -1298,9 +1341,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
dataSource
}
+ val dafuckDataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString()))
cacheFactory = CacheDataSource.Factory().apply {
- setCache(simpleCache)
- setUpstreamDataSourceFactory(dataSourceFactory)
+ setCache(Helper.getSimpleCache(this@ExoplayerView))
+ if (ext.server.offline) {
+ setUpstreamDataSourceFactory(dafuckDataSourceFactory)
+ } else {
+ setUpstreamDataSourceFactory(dataSourceFactory)
+ }
+ setCacheWriteDataSinkFactory(null)
}
val mimeType = when (video?.format) {
@@ -1309,15 +1358,40 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
else -> MimeTypes.APPLICATION_MP4
}
- val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
- logger("url: ${video!!.file.url}")
- logger("mimeType: $mimeType")
+ val downloadedMediaItem = if (ext.server.offline) {
+ val key = ext.server.name
+ downloadId = getSharedPreferences(getString(R.string.anime_downloads), MODE_PRIVATE)
+ .getString(key, null)
+ if (downloadId != null) {
+ Helper.downloadManager(this)
+ .downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
+ } else {
+ snackString("Download not found")
+ null
+ }
+ } else null
+
+ mediaItem = if (downloadedMediaItem == null) {
+ val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
+ logger("url: ${video!!.file.url}")
+ logger("mimeType: $mimeType")
- if (sub != null) {
- val listofnotnullsubs = immutableListOf(sub).filterNotNull()
- builder.setSubtitleConfigurations(listofnotnullsubs)
+ if (sub != null) {
+ val listofnotnullsubs = immutableListOf(sub).filterNotNull()
+ builder.setSubtitleConfigurations(listofnotnullsubs)
+ }
+ builder.build()
+ } else {
+ val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
+ if (sub != null) {
+ val listofnotnullsubs = immutableListOf(sub).filterNotNull()
+ val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build()
+ addedSubsDownloadedMediaItem.setSubtitleConfigurations(immutableListOf(addLanguage))
+ episode.selectedSubtitle = 0
+ }
+ addedSubsDownloadedMediaItem.build()
}
- mediaItem = builder.build()
+
//Source
exoSource.setOnClickListener {
@@ -1439,7 +1513,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
exoPlayer.release()
VideoCache.release()
mediaSession?.release()
- if(DiscordServiceRunningSingleton.running) {
+ if (DiscordServiceRunningSingleton.running) {
val stopIntent = Intent(this, DiscordService::class.java)
DiscordServiceRunningSingleton.running = false
stopService(stopIntent)
@@ -1479,7 +1553,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
super.onPause()
orientationListener?.disable()
if (isInitialized) {
- playerView.player?.pause()
+ if (castPlayer?.isPlaying == false) {
+ playerView.player?.pause()
+ }
saveData(
"${media.id}_${media.anime!!.selectedEpisode}",
exoPlayer.currentPosition,
@@ -1500,7 +1576,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
override fun onStop() {
- playerView.player?.pause()
+ if (castPlayer?.isPlaying == false) {
+ playerView.player?.pause()
+ }
super.onStop()
}
@@ -1793,7 +1871,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
// Enter PiP Mode
@Suppress("DEPRECATION")
- @RequiresApi(Build.VERSION_CODES.N)
private fun enterPipMode() {
wasPlaying = isPlayerPlaying
if (!pipEnabled) return
@@ -1866,6 +1943,55 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
}
+
+ private fun startCastPlayer() {
+ if (!isCastApiAvailable) {
+ snackString("Cast API not available")
+ return
+ }
+ //make sure mediaItem is initialized and castPlayer is not null
+ if (!this::mediaItem.isInitialized || castPlayer == null) return
+ castPlayer?.setMediaItem(mediaItem)
+ castPlayer?.prepare()
+ playerView.player = castPlayer
+ exoPlayer.stop()
+ castPlayer?.addListener(object : Player.Listener {
+ //if the player is paused changed, we want to update the UI
+ override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
+ super.onPlayWhenReadyChanged(playWhenReady, reason)
+ if (playWhenReady) {
+ (exoPlay.drawable as Animatable?)?.start()
+ Glide.with(this@ExoplayerView)
+ .load(R.drawable.anim_play_to_pause)
+ .into(exoPlay)
+ } else {
+ (exoPlay.drawable as Animatable?)?.start()
+ Glide.with(this@ExoplayerView)
+ .load(R.drawable.anim_pause_to_play)
+ .into(exoPlay)
+ }
+ }
+ })
+ }
+
+ private fun startExoPlayer() {
+ exoPlayer.setMediaItem(mediaItem)
+ exoPlayer.prepare()
+ playerView.player = exoPlayer
+ castPlayer?.stop()
+ }
+
+ override fun onCastSessionAvailable() {
+ if (isCastApiAvailable) {
+ startCastPlayer()
+ }
+ }
+
+ override fun onCastSessionUnavailable() {
+ startExoPlayer()
+ }
+
+
@SuppressLint("ViewConstructor")
class ExtendedTimeBar(
context: Context,
diff --git a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt
index d42bcd948c9..688222c5859 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt
@@ -1,6 +1,7 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
+import android.app.AlertDialog
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color
@@ -20,17 +21,21 @@ import ani.dantotsu.*
import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.databinding.ItemStreamBinding
import ani.dantotsu.databinding.ItemUrlBinding
+import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.Download.download
+import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType
+import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.DecimalFormat
+
class SelectorDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSelectorBinding? = null
private val binding get() = _binding!!
@@ -42,6 +47,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
private var makeDefault = false
private var selected: String? = null
private var launch: Boolean? = null
+ private var isDownloadMenu: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -49,6 +55,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
selected = it.getString("server")
launch = it.getBoolean("launch", true)
prevEpisode = it.getString("prev")
+ isDownloadMenu = it.getBoolean("isDownload")
}
}
@@ -76,8 +83,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
episode = ep
if (ep != null) {
+ if (isDownloadMenu == true) {
+ binding.selectorMakeDefault.visibility = View.GONE
+ }
- if (selected != null) {
+ if (selected != null && isDownloadMenu == false) {
binding.selectorListContainer.visibility = View.GONE
binding.selectorAutoListContainer.visibility = View.VISIBLE
binding.selectorAutoText.text = selected
@@ -95,7 +105,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
fun load() {
val size =
- ep.extractors?.find { it.server.name == selected }?.videos?.size
+ if (model.watchSources!!.isDownloadedSource(media!!.selected!!.sourceIndex)) {
+ ep.extractors?.firstOrNull()?.videos?.size
+ } else {
+ ep.extractors?.find { it.server.name == selected }?.videos?.size
+ }
+
if (size != null && size >= media!!.selected!!.video) {
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
selected
@@ -145,6 +160,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
ep.extractorCallback = {
scope.launch {
adapter.add(it)
+ if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
+ adapter.performClick(0)
+ }
}
}
model.getEpisode().observe(this) {
@@ -164,6 +182,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} else {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
adapter.addAll(ep.extractors)
+ if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
+ adapter.performClick(0)
+ }
binding.selectorProgressBar.visibility = View.GONE
}
}
@@ -179,7 +200,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
prevEpisode = null
dismiss()
- if (launch!!) {
+ if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
stopAddingToList()
val intent = Intent(activity, ExoplayerView::class.java)
ExoplayerView.media = media
@@ -214,7 +235,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val extractor = links[position]
- holder.binding.streamName.text = extractor.server.name
+ holder.binding.streamName.text = ""//extractor.server.name
+ holder.binding.streamName.visibility = View.GONE
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
@@ -235,6 +257,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
notifyItemRangeInserted(0, extractors.size)
}
+ fun performClick(position: Int) {
+ try { //bandaid fix for crash
+ val extractor = links[position]
+ media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
+ extractor.server.name
+ media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = 0
+ startExoplayer(media!!)
+ } catch (e: Exception) {
+ FirebaseCrashlytics.getInstance().recordException(e)
+ }
+ }
+
private inner class StreamViewHolder(val binding: ItemStreamBinding) :
RecyclerView.ViewHolder(binding.root)
}
@@ -256,24 +290,107 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
val binding = holder.binding
val video = extractor.videos[position]
- binding.urlQuality.text =
- if (video.quality != null) "${video.quality}p" else "Default Quality"
- binding.urlNote.text = video.extraNote ?: ""
- binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
- binding.urlDownload.visibility = View.VISIBLE
+ if (isDownloadMenu == true) {
+ binding.urlDownload.visibility = View.VISIBLE
+ } else {
+ binding.urlDownload.visibility = View.GONE
+ }
binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
position
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
- download(
- requireActivity(),
- media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
- media!!.userPreferredName
- )
+ 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
+ if (subtitles.isNotEmpty()) {
+ val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
+ .setTitle("Download Subtitle")
+ .setSingleChoiceItems(
+ subtitleNames.toTypedArray(),
+ -1
+ ) { dialog, which ->
+ subtitleToDownload = subtitles[which]
+ }
+ .setPositiveButton("Download") { _, _ ->
+ dialog?.dismiss()
+ if (selectedVideo != null) {
+ Helper.startAnimeDownloadService(
+ currActivity()!!,
+ media!!.mainName(),
+ episode.number,
+ selectedVideo,
+ subtitleToDownload,
+ media,
+ episode.thumb?.url ?: media!!.banner ?: media!!.cover
+ )
+ } else {
+ snackString("No Video Selected")
+ }
+ }
+ .setNegativeButton("Skip") { dialog, _ ->
+ subtitleToDownload = null
+ if (selectedVideo != null) {
+ Helper.startAnimeDownloadService(
+ currActivity()!!,
+ media!!.mainName(),
+ episode.number,
+ selectedVideo,
+ subtitleToDownload,
+ media,
+ episode.thumb?.url ?: media!!.banner ?: media!!.cover
+ )
+ } else {
+ snackString("No Video Selected")
+ }
+ dialog.dismiss()
+ }
+ .setNeutralButton("Cancel") { dialog, _ ->
+ subtitleToDownload = null
+ dialog.dismiss()
+ }
+ .show()
+ alertDialog.window?.setDimAmount(0.8f)
+
+ } else {
+ if (selectedVideo != null) {
+ Helper.startAnimeDownloadService(
+ requireActivity(),
+ media!!.mainName(),
+ episode.number,
+ selectedVideo,
+ subtitleToDownload,
+ media,
+ episode.thumb?.url ?: media!!.banner ?: media!!.cover
+ )
+ } else {
+ snackString("No Video Selected")
+ }
+ }
dismiss()
}
+ binding.urlDownload.setOnLongClickListener {
+ binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ if ((loadData("settings_download_manager") ?: 0) != 0) {
+ media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
+ extractor.server.name
+ media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
+ position
+ download(
+ requireActivity(),
+ media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
+ media!!.userPreferredName
+ )
+ } else {
+ snackString("No Download Manager Selected")
+ }
+ true
+ }
if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
binding.urlSize.text =
@@ -281,12 +398,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
"#.##"
).format(video.size ?: 0).toString() + " MB"))
- } else {
- binding.urlQuality.text = "Multi Quality"
- if ((loadData("settings_download_manager") ?: 0) == 0) {
- binding.urlDownload.visibility = View.GONE
- }
}
+ binding.urlNote.visibility = View.VISIBLE
+ binding.urlNote.text = video.format.name
+ binding.urlQuality.text = extractor.server.name
}
override fun getItemCount(): Int = extractor.videos.size
@@ -295,6 +410,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setSafeOnClickListener {
+ if (isDownloadMenu == true) {
+ binding.urlDownload.performClick()
+ return@setSafeOnClickListener
+ }
tryWith(true) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
extractor.server.name
@@ -326,13 +445,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
fun newInstance(
server: String? = null,
la: Boolean = true,
- prev: String? = null
+ prev: String? = null,
+ isDownload: Boolean
): SelectorDialogFragment =
SelectorDialogFragment().apply {
arguments = Bundle().apply {
putString("server", server)
putBoolean("launch", la)
putString("prev", prev)
+ putBoolean("isDownload", isDownload)
}
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt
index 406efb3cb2e..6cc971574fa 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt
@@ -6,7 +6,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.annotation.OptIn
import androidx.fragment.app.activityViewModels
+import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment
@@ -60,6 +62,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
)
)
+ @OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding
if (position == 0) {
diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt
index fb921a9fd3e..9eb14a6ca23 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt
@@ -141,8 +141,8 @@ class MangaChapterAdapter(
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf()
- val typedValue1 = TypedValue()
- val typedValue2 = TypedValue()
+ private val typedValue1 = TypedValue()
+ private val typedValue2 = TypedValue()
fun bind(chapterNumber: String, progress: String?) {
if (progress != null) {
binding.itemChapterTitle.visibility = View.VISIBLE
@@ -167,6 +167,7 @@ class MangaChapterAdapter(
} else {
// Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
+ binding.itemDownload.rotation = 0f
}
}
@@ -235,7 +236,7 @@ class MangaChapterAdapter(
input.maxValue = itemCount - bindingAdapterPosition
input.value = 1
alertDialog.setView(input)
- alertDialog.setPositiveButton("OK") { dialog, which ->
+ alertDialog.setPositiveButton("OK") { _, _ ->
downloadNChaptersFrom(bindingAdapterPosition, input.value)
}
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaNameAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaNameAdapter.kt
index 05e9b360da2..ae755faa874 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/MangaNameAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/MangaNameAdapter.kt
@@ -5,16 +5,25 @@ import java.util.regex.Pattern
class MangaNameAdapter {
companion object {
+ const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*"
+ const val filedChapterNumberRegex = "(?= mangaReadSources.names.size) 0 else it }
@@ -117,7 +136,7 @@ class MangaReadAdapter(
}
}
- //Subscription
+ //Grids
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
binding.animeSourceSubscribe,
@@ -136,98 +155,156 @@ class MangaReadAdapter(
openSettings(fragment.requireContext(), getChannelId(true, media.id))
}
- //Icons
- binding.animeSourceGrid.visibility = View.GONE
- var reversed = media.selected!!.recyclerReversed
- var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
- binding.animeSourceTop.rotation = if (reversed) -90f else 90f
- binding.animeSourceTop.setOnClickListener {
- reversed = !reversed
- binding.animeSourceTop.rotation = if (reversed) -90f else 90f
- fragment.onIconPressed(style, reversed)
- }
+ binding.animeNestedButton.setOnClickListener {
- binding.animeScanlatorTop.setOnClickListener {
val dialogView =
- LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
- val checkboxContainer = dialogView.findViewById(R.id.checkboxContainer)
-
- // Dynamically add checkboxes
+ LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
+ val dialogBinding = DialogLayoutBinding.bind(dialogView)
+ var refresh = false
+ var run = false
+ var reversed = media.selected!!.recyclerReversed
+ var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
+ dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
+ dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
+ dialogBinding.animeSourceTop.setOnClickListener {
+ reversed = !reversed
+ dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
+ dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
+ run = true
+ }
- options.forEach { option ->
- val checkBox = CheckBox(currContext()).apply {
- text = option
+ //Grids
+ dialogBinding.animeSourceGrid.visibility = View.GONE
+ var selected = when (style) {
+ 0 -> dialogBinding.animeSourceList
+ 1 -> dialogBinding.animeSourceCompact
+ else -> dialogBinding.animeSourceList
+ }
+ when (style) {
+ 0 -> dialogBinding.layoutText.text = "List"
+ 1 -> dialogBinding.layoutText.text = "Compact"
+ else -> dialogBinding.animeSourceList
+ }
+ selected.alpha = 1f
+ fun selected(it: ImageButton) {
+ selected.alpha = 0.33f
+ selected = it
+ selected.alpha = 1f
+ }
+ dialogBinding.animeSourceList.setOnClickListener {
+ selected(it as ImageButton)
+ style = 0
+ dialogBinding.layoutText.text = "List"
+ run = true
+ }
+ dialogBinding.animeSourceCompact.setOnClickListener {
+ selected(it as ImageButton)
+ style = 1
+ dialogBinding.layoutText.text = "Compact"
+ run = true
+ }
+ dialogBinding.animeWebviewContainer.setOnClickListener {
+ if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
+ toast("WebView not installed")
}
- //set checked if it's already selected
- if (media.selected!!.scanlators != null) {
- checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
- scanlatorSelectionListener?.onScanlatorsSelected()
- } else {
- checkBox.isChecked = true
+ //start CookieCatcher activity
+ if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
+ val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
+ val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
+ val url = sourceHttp?.baseUrl
+ url?.let {
+ refresh = true
+ val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
+ .putExtra("url", url)
+ ContextCompat.startActivity(fragment.requireContext(), intent, null)
+ }
}
- checkboxContainer.addView(checkBox)
}
- // Create AlertDialog
- val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
- .setView(dialogView)
- .setPositiveButton("OK") { dialog, which ->
- //add unchecked to hidden
- hiddenScanlators.clear()
- for (i in 0 until checkboxContainer.childCount) {
- val checkBox = checkboxContainer.getChildAt(i) as CheckBox
- if (!checkBox.isChecked) {
- hiddenScanlators.add(checkBox.text.toString())
- }
+ //Multi download
+ dialogBinding.downloadNo.text = "0"
+ dialogBinding.animeDownloadTop.setOnClickListener {
+ //Alert dialog asking for the number of chapters to download
+ val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
+ alertDialog.setTitle("Multi Chapter Downloader")
+ alertDialog.setMessage("Enter the number of chapters to download")
+ val input = NumberPicker(currContext())
+ input.minValue = 1
+ input.maxValue = 20
+ input.value = 1
+ alertDialog.setView(input)
+ alertDialog.setPositiveButton("OK") { _, _ ->
+ dialogBinding.downloadNo.text = "${input.value}"
+ }
+ alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
+ val dialog = alertDialog.show()
+ dialog.window?.setDimAmount(0.8f)
+ }
+
+ //Scanlator
+ dialogBinding.animeScanlatorContainer.visibility =
+ if (options.count() > 1) View.VISIBLE else View.GONE
+ dialogBinding.scanlatorNo.text = "${options.count()}"
+ dialogBinding.animeScanlatorTop.setOnClickListener {
+ val dialogView2 =
+ LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
+ val checkboxContainer =
+ dialogView2.findViewById(R.id.checkboxContainer)
+
+ // Dynamically add checkboxes
+ options.forEach { option ->
+ val checkBox = CheckBox(currContext()).apply {
+ text = option
}
- fragment.onScanlatorChange(hiddenScanlators)
- scanlatorSelectionListener?.onScanlatorsSelected()
+ //set checked if it's already selected
+ if (media.selected!!.scanlators != null) {
+ checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
+ scanlatorSelectionListener?.onScanlatorsSelected()
+ } else {
+ checkBox.isChecked = true
+ }
+ checkboxContainer.addView(checkBox)
}
- .setNegativeButton("Cancel", null)
- .show()
- dialog.window?.setDimAmount(0.8f)
- }
- binding.animeDownloadTop.setOnClickListener {
- //Alert dialog asking for the number of chapters to download
- val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
- alertDialog.setTitle("Multi Chapter Downloader")
- alertDialog.setMessage("Enter the number of chapters to download")
- val input = NumberPicker(currContext())
- input.minValue = 1
- input.maxValue = 20
- input.value = 1
- alertDialog.setView(input)
- alertDialog.setPositiveButton("OK") { dialog, which ->
- fragment.multiDownload(input.value)
+ // Create AlertDialog
+ val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
+ .setView(dialogView2)
+ .setPositiveButton("OK") { _, _ ->
+ //add unchecked to hidden
+ hiddenScanlators.clear()
+ for (i in 0 until checkboxContainer.childCount) {
+ val checkBox = checkboxContainer.getChildAt(i) as CheckBox
+ if (!checkBox.isChecked) {
+ hiddenScanlators.add(checkBox.text.toString())
+ }
+ }
+ fragment.onScanlatorChange(hiddenScanlators)
+ scanlatorSelectionListener?.onScanlatorsSelected()
+ }
+ .setNegativeButton("Cancel", null)
+ .show()
+ dialog.window?.setDimAmount(0.8f)
}
- alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
- val dialog = alertDialog.show()
- dialog.window?.setDimAmount(0.8f)
- }
- var selected = when (style) {
- 0 -> binding.animeSourceList
- 1 -> binding.animeSourceCompact
- else -> binding.animeSourceList
- }
- selected.alpha = 1f
- fun selected(it: ImageView) {
- selected.alpha = 0.33f
- selected = it
- selected.alpha = 1f
- }
- binding.animeSourceList.setOnClickListener {
- selected(it as ImageView)
- style = 0
- fragment.onIconPressed(style, reversed)
- }
- binding.animeSourceCompact.setOnClickListener {
- selected(it as ImageView)
- style = 1
- fragment.onIconPressed(style, reversed)
+ nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
+ .setTitle("Options")
+ .setView(dialogView)
+ .setPositiveButton("OK") { _, _ ->
+ if (run) fragment.onIconPressed(style, reversed)
+ if (dialogBinding.downloadNo.text != "0") {
+ fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
+ }
+ if (refresh) fragment.loadChapters(source, true)
+ }
+ .setNegativeButton("Cancel") { _, _ ->
+ if (refresh) fragment.loadChapters(source, true)
+ }
+ .setOnCancelListener {
+ if (refresh) fragment.loadChapters(source, true)
+ }
+ .create()
+ nestedDialog?.show()
}
-
//Chapter Handling
handleChapters()
}
@@ -259,6 +336,7 @@ class MangaReadAdapter(
0
)
}
+
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
val startChapterString = if (startChapter != null) {
@@ -370,7 +448,7 @@ class MangaReadAdapter(
}
}
- fun setLanguageList(lang: Int, source: Int) {
+ private fun setLanguageList(lang: Int, source: Int) {
val binding = _binding
if (mangaReadSources is MangaSources) {
val parser = mangaReadSources[source] as? DynamicMangaParser
@@ -385,12 +463,16 @@ class MangaReadAdapter(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
}
- binding?.animeSourceLanguage?.setAdapter(
- ArrayAdapter(
- fragment.requireContext(),
- R.layout.item_dropdown,
- parser.extension.sources.map { it.lang })
+ val adapter = ArrayAdapter(
+ fragment.requireContext(),
+ R.layout.item_dropdown,
+ parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
)
+ val items = adapter.count
+ binding?.animeSourceLanguageContainer?.visibility =
+ if (items > 1) View.VISIBLE else View.GONE
+
+ binding?.animeSourceLanguage?.setAdapter(adapter)
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
index 25a87c9bc16..3153bb6a72e 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
@@ -29,7 +29,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
-import ani.dantotsu.download.Download
+import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.MangaServiceDataSingleton
@@ -37,6 +37,7 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
+import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser
@@ -166,7 +167,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
chapterAdapter =
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
- for (download in downloadManager.mangaDownloads) {
+ for (download in downloadManager.mangaDownloadedTypes) {
chapterAdapter.stopDownload(download.chapter)
}
@@ -202,20 +203,25 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val selected = media.userProgress
val chapters = media.manga?.chapters?.values?.toList()
//filter by selected language
- val progressChapterIndex = chapters?.indexOfFirst { MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected }?:0
- if (progressChapterIndex < 0 || n < 1) return
- val chaptersToDownload = chapters?.subList(
- progressChapterIndex + 1,
- progressChapterIndex + n + 1
- )
- if (chaptersToDownload != null) {
- for (chapter in chaptersToDownload) {
- onMangaChapterDownloadClick(chapter.title!!)
- }
- }
+ val progressChapterIndex = (chapters?.indexOfFirst {
+ MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
+ } ?: 0) + 1
+
+ if (progressChapterIndex < 0 || n < 1 || chapters == null) return
+
+ // Calculate the end index
+ val endIndex = minOf(progressChapterIndex + n, chapters.size)
+
+ //make sure there are enough chapters
+ val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
+
+ for (chapter in chaptersToDownload) {
+ onMangaChapterDownloadClick(chapter.title!!)
+ }
}
+
private fun updateChapters() {
val loadedChapters = model.getMangaChapters().value
if (loadedChapters != null) {
@@ -260,7 +266,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
}
- fun getScanlators(chap: MutableMap?): List {
+ private fun getScanlators(chap: MutableMap?): List {
val scanlators = mutableListOf()
if (chap != null) {
val chapters = chap.values
@@ -355,19 +361,20 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
if (show) View.GONE else View.VISIBLE
}
}
+ var itemSelected = false
val allSettings = pkg.sources.filterIsInstance()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
- val names = allSettings.map { it.lang }.toTypedArray()
+ val names =
+ allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
var selectedIndex = 0
- val dialog = AlertDialog.Builder(requireContext())
+ val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
- .setSingleChoiceItems(names, selectedIndex) { _, which ->
+ .setSingleChoiceItems(names, selectedIndex) { dialog, which ->
selectedIndex = which
- }
- .setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
+ itemSelected = true
dialog.dismiss()
// Move the fragment transaction here
@@ -382,10 +389,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
.addToBackStack(null)
.commit()
}
- .setNegativeButton("Cancel") { dialog, _ ->
- dialog.cancel()
- changeUIVisibility(true)
- return@setNegativeButton
+ .setOnDismissListener {
+ if (!itemSelected) {
+ changeUIVisibility(true)
+ }
}
.show()
dialog.window?.setDimAmount(0.8f)
@@ -440,7 +447,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Create a download task
val downloadTask = MangaDownloaderService.DownloadTask(
- title = media.nameMAL ?: media.nameRomaji,
+ title = media.mainName(),
chapter = chapter.title!!,
imageData = images,
sourceMedia = media,
@@ -481,10 +488,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
fun onMangaChapterRemoveDownloadClick(i: String) {
downloadManager.removeDownload(
- Download(
- media.nameMAL ?: media.nameRomaji,
+ DownloadedType(
+ media.mainName(),
i,
- Download.Type.MANGA
+ DownloadedType.Type.MANGA
)
)
chapterAdapter.deleteDownload(i)
@@ -499,10 +506,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Remove the download from the manager and update the UI
downloadManager.removeDownload(
- Download(
- media.nameMAL ?: media.nameRomaji,
+ DownloadedType(
+ media.mainName(),
i,
- Download.Type.MANGA
+ DownloadedType.Type.MANGA
)
)
chapterAdapter.purgeDownload(i)
diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt
index 6a18b9e8625..952db6195c8 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt
@@ -89,7 +89,8 @@ abstract class BaseImageAdapter(
}
} else {
val detector = GestureDetectorCompat(view.context, object : GesturesListener() {
- override fun onSingleClick(event: MotionEvent) = activity.handleController()
+ override fun onSingleClick(event: MotionEvent) =
+ activity.handleController(event = event)
})
view.findViewById(R.id.imgProgCover).apply {
setOnTouchListener { _, event ->
@@ -112,6 +113,9 @@ abstract class BaseImageAdapter(
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) }
}
+ abstract fun isZoomed(): Boolean
+ abstract fun setZoom(zoom: Float)
+
abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object {
diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt
index 8d31e9d1846..e1bebb83ba8 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt
@@ -50,7 +50,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
if (model.loadMangaChapterImages(
chp,
m.selected!!,
- m.nameMAL ?: m.nameRomaji
+ m.mainName()
)
) {
val activity = currActivity()
diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt
index 9765149959b..58b48038a82 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt
@@ -91,4 +91,16 @@ open class ImageAdapter(
}
override fun getItemCount(): Int = images.size
+
+ override fun isZoomed(): Boolean {
+ val imageView =
+ activity.findViewById(R.id.imgProgImageNoGestures)
+ return imageView.scale > imageView.minScale
+ }
+
+ override fun setZoom(zoom: Float) {
+ val imageView =
+ activity.findViewById(R.id.imgProgImageNoGestures)
+ imageView.setScaleAndCenter(zoom, imageView.center)
+ }
}
diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt
index 30fefb26524..c396088de2d 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt
@@ -3,8 +3,10 @@ package ani.dantotsu.media.manga.mangareader
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.content.res.Resources
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
@@ -218,7 +220,7 @@ class MangaReaderActivity : AppCompatActivity() {
val mangaSources = MangaSources
val scope = lifecycleScope
scope.launch(Dispatchers.IO) {
- mangaSources.init(Injekt.get().installedExtensionsFlow)
+ mangaSources.init(Injekt.get().installedExtensionsFlow, this@MangaReaderActivity)
}
model.mangaReadSources = mangaSources
} else {
@@ -253,7 +255,8 @@ class MangaReaderActivity : AppCompatActivity() {
}
showProgressDialog =
- if (settings.askIndividual) loadData("${media.id}_progressDialog") != true else false
+ if (settings.askIndividual) loadData("${media.id}_progressDialog")
+ ?: true else false
//Chapter Change
fun change(index: Int) {
@@ -358,7 +361,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages(
chapter,
media.selected!!,
- media.nameMAL ?: media.nameRomaji
+ media.mainName()
)
}
}
@@ -701,8 +704,60 @@ class MangaReaderActivity : AppCompatActivity() {
goneTimer.schedule(timerTask, controllerDuration)
}
- fun handleController(shouldShow: Boolean? = null) {
+ enum class pressPos {
+ LEFT, RIGHT, CENTER
+ }
+
+ fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
+ var pressLocation = pressPos.CENTER
if (!sliding) {
+ if (event != null && settings.default.layout == PAGED) {
+ if (event.action != MotionEvent.ACTION_UP) return
+ val x = event.rawX.toInt()
+ val y = event.rawY.toInt()
+ val screenWidth = Resources.getSystem().displayMetrics.widthPixels
+ //if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
+ if (screenWidth / 5 in (x + 1).. screenWidth - screenWidth / 5 && y > screenWidth / 5) {
+ pressLocation = if (settings.default.direction == RIGHT_TO_LEFT) {
+ pressPos.LEFT
+ } else {
+ pressPos.RIGHT
+ }
+ }
+ }
+
+ // if pressLocation is left or right go to previous or next page (paged mode only)
+ if (pressLocation == pressPos.LEFT) {
+
+ if (binding.mangaReaderPager.currentItem > 0) {
+ //if the current images zoomed in, go back to normal before going to previous page
+ if (imageAdapter?.isZoomed() == true) {
+ imageAdapter?.setZoom(1f)
+ }
+ binding.mangaReaderPager.currentItem -= 1
+ return
+ }
+
+ } else if (pressLocation == pressPos.RIGHT) {
+ if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
+ //if the current images zoomed in, go back to normal before going to next page
+ if (imageAdapter?.isZoomed() == true) {
+ imageAdapter?.setZoom(1f)
+ }
+ //if right to left, go to previous page
+ binding.mangaReaderPager.currentItem += 1
+ return
+ }
+ }
+
if (!settings.showSystemBars) {
hideBars()
checkNotch()
@@ -786,7 +841,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!,
- media.nameMAL ?: media.nameRomaji,
+ media.mainName(),
false
)
loading = false
@@ -795,17 +850,28 @@ class MangaReaderActivity : AppCompatActivity() {
private fun progress(runnable: Runnable) {
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
+ showProgressDialog =
+ if (settings.askIndividual) loadData("${media.id}_progressDialog")
+ ?: true else false
if (showProgressDialog) {
+
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
val checkbox = dialogView.findViewById(R.id.dialog_checkbox)
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
checkbox.setOnCheckedChangeListener { _, isChecked ->
- saveData("${media.id}_progressDialog", isChecked)
+ saveData("${media.id}_progressDialog", !isChecked)
showProgressDialog = !isChecked
}
-
+ val incognito =
+ currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getBoolean("incognito", false) ?: false
AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.title_update_progress))
+ .apply {
+ if (incognito) {
+ setMessage(getString(R.string.incognito_will_not_update))
+ }
+ }
.setView(dialogView)
.setCancelable(false)
.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
index 7154412ab27..0c654c16c49 100644
--- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
@@ -22,7 +22,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
-import ani.dantotsu.download.Download
+import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton
@@ -67,7 +67,7 @@ class NovelReadFragment : Fragment(),
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
Log.e("downloadTrigger", novelDownloadPackage.link)
val downloadTask = NovelDownloaderService.DownloadTask(
- title = media.nameMAL ?: media.nameRomaji,
+ title = media.mainName(),
chapter = novelDownloadPackage.novelName,
downloadLink = novelDownloadPackage.link,
originalLink = novelDownloadPackage.originalLink,
@@ -92,16 +92,16 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get()
if (downloadsManager.queryDownload(
- Download(
- media.nameMAL ?: media.nameRomaji,
+ DownloadedType(
+ media.mainName(),
novel.name,
- Download.Type.NOVEL
+ DownloadedType.Type.NOVEL
)
)
) {
val file = File(
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "${DownloadsManager.novelLocation}/${media.nameMAL ?: media.nameRomaji}/${novel.name}/0.epub"
+ "${DownloadsManager.novelLocation}/${media.mainName()}/${novel.name}/0.epub"
)
if (!file.exists()) return false
val fileUri = FileProvider.getUriForFile(
@@ -124,10 +124,10 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheck(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get()
return downloadsManager.queryDownload(
- Download(
- media.nameMAL ?: media.nameRomaji,
+ DownloadedType(
+ media.mainName(),
novel.name,
- Download.Type.NOVEL
+ DownloadedType.Type.NOVEL
)
)
}
@@ -135,10 +135,10 @@ class NovelReadFragment : Fragment(),
override fun deleteDownload(novel: ShowResponse) {
val downloadsManager = Injekt.get()
downloadsManager.removeDownload(
- Download(
- media.nameMAL ?: media.nameRomaji,
+ DownloadedType(
+ media.mainName(),
novel.name,
- Download.Type.NOVEL
+ DownloadedType.Type.NOVEL
)
)
}
@@ -247,8 +247,7 @@ class NovelReadFragment : Fragment(),
headerAdapter.progress?.visibility = View.VISIBLE
lifecycleScope.launch(Dispatchers.IO) {
if (auto || query == "") model.autoSearchNovels(media)
- //else model.searchNovels(query, source)
- else model.autoSearchNovels(media) //testing
+ else model.searchNovels(query, source)
}
searching = true
if (save) {
diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt
index 5f99681f27b..6d57037456c 100644
--- a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt
@@ -42,7 +42,11 @@ class NovelResponseAdapter(
.into(binding.itemEpisodeImage)
val typedValue = TypedValue()
- fragment.requireContext().theme?.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue, true)
+ fragment.requireContext().theme?.resolveAttribute(
+ com.google.android.material.R.attr.colorOnBackground,
+ typedValue,
+ true
+ )
val color = typedValue.data
binding.itemEpisodeTitle.text = novel.name
@@ -98,14 +102,19 @@ class NovelResponseAdapter(
}
binding.root.setOnLongClickListener {
- val builder = androidx.appcompat.app.AlertDialog.Builder(fragment.requireContext(), R.style.DialogTheme)
+ val builder = androidx.appcompat.app.AlertDialog.Builder(
+ fragment.requireContext(),
+ R.style.DialogTheme
+ )
builder.setTitle("Delete ${novel.name}?")
builder.setMessage("Are you sure you want to delete ${novel.name}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadedCheckCallback.deleteDownload(novel)
deleteDownload(novel.link)
snackString("Deleted ${novel.name}")
- if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) {
+ if (binding.itemEpisodeFiller.text.toString()
+ .contains("Download", ignoreCase = true)
+ ) {
binding.itemEpisodeFiller.text = ""
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt
index 069fef2820c..b62032fae18 100644
--- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt
@@ -15,6 +15,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.OvershootInterpolator
+import android.webkit.WebView
import android.widget.AdapterView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
@@ -36,7 +37,7 @@ import ani.dantotsu.saveData
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.CurrentNovelReaderSettings
import ani.dantotsu.settings.CurrentReaderSettings
-import ani.dantotsu.settings.NovelReaderSettings
+import ani.dantotsu.settings.ReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
@@ -62,7 +63,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
private lateinit var binding: ActivityNovelReaderBinding
private val scope = lifecycleScope
- lateinit var settings: NovelReaderSettings
+ lateinit var settings: ReaderSettings
private lateinit var uiSettings: UserInterfaceSettings
private var notchHeight: Int? = null
@@ -139,16 +140,31 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
}
+ @SuppressLint("WebViewApiAvailability")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//check for supported webview
- val webViewVersion = WebViewCompat.getCurrentWebViewPackage(this)?.versionName
+ val webViewVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ WebView.getCurrentWebViewPackage()?.versionName
+ } else {
+ WebViewCompat.getCurrentWebViewPackage(this)?.versionName
+ }
val firstVersion = webViewVersion?.split(".")?.firstOrNull()?.toIntOrNull()
if (webViewVersion == null || firstVersion == null || firstVersion < 87) {
- Toast.makeText(this, "Please update WebView from PlayStore", Toast.LENGTH_LONG).show()
+ val text = if (webViewVersion == null) {
+ "Could not find webView installed"
+ } else if (firstVersion == null) {
+ "Could not find WebView Version Number: $webViewVersion"
+ } else if (firstVersion < 87) { //false positive?
+ "Webview Versiom: $firstVersion. PLease update"
+ } else {
+ "Please update WebView from PlayStore"
+ }
+ Toast.makeText(this, text, Toast.LENGTH_LONG).show()
//open playstore
val intent = Intent(Intent.ACTION_VIEW)
- intent.data = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview")
+ intent.data =
+ Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview")
startActivity(intent)
//stop reader
finish()
@@ -159,9 +175,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
ThemeManager(this).applyTheme()
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
setContentView(binding.root)
-
- settings = loadData("novel_reader_settings", this)
- ?: NovelReaderSettings().apply { saveData("novel_reader_settings", this) }
+ settings = loadData("reader_settings", this)
+ ?: ReaderSettings().apply { saveData("reader_settings", this) }
uiSettings = loadData("ui_settings", this)
?: UserInterfaceSettings().also { saveData("ui_settings", it) }
@@ -271,7 +286,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
binding.bookReader.getAppearance {
currentTheme = it
themes.add(0, it)
- settings.default = loadData("${sanitizedBookId}_current_settings") ?: settings.default
+ settings.defaultLN =
+ loadData("${sanitizedBookId}_current_settings") ?: settings.defaultLN
applySettings()
}
@@ -323,7 +339,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> {
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP)
- if (!settings.default.volumeButtons)
+ if (!settings.defaultLN.volumeButtons)
return false
if (event.action == KeyEvent.ACTION_DOWN) {
onVolumeUp?.invoke()
@@ -333,7 +349,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> {
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
- if (!settings.default.volumeButtons)
+ if (!settings.defaultLN.volumeButtons)
return false
if (event.action == KeyEvent.ACTION_DOWN) {
onVolumeDown?.invoke()
@@ -349,13 +365,18 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
fun applySettings() {
- saveData("${sanitizedBookId}_current_settings", settings.default)
+ saveData("${sanitizedBookId}_current_settings", settings.defaultLN)
hideBars()
+ if (settings.defaultLN.useOledTheme) {
+ themes.forEach { theme ->
+ theme.darkBg = Color.parseColor("#000000")
+ }
+ }
currentTheme =
- themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) }
+ themes.first { it.name.equals(settings.defaultLN.currentThemeName, ignoreCase = true) }
- when (settings.default.layout) {
+ when (settings.defaultLN.layout) {
CurrentNovelReaderSettings.Layouts.PAGED -> {
currentTheme?.flow = ReaderFlow.PAGINATED
}
@@ -366,22 +387,22 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
- when (settings.default.dualPageMode) {
+ when (settings.defaultLN.dualPageMode) {
CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1
CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2
CurrentReaderSettings.DualPageModes.Force -> requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
- currentTheme?.lineHeight = settings.default.lineHeight
- currentTheme?.gap = settings.default.margin
- currentTheme?.maxInlineSize = settings.default.maxInlineSize
- currentTheme?.maxBlockSize = settings.default.maxBlockSize
- currentTheme?.useDark = settings.default.useDarkTheme
+ currentTheme?.lineHeight = settings.defaultLN.lineHeight
+ currentTheme?.gap = settings.defaultLN.margin
+ currentTheme?.maxInlineSize = settings.defaultLN.maxInlineSize
+ currentTheme?.maxBlockSize = settings.defaultLN.maxBlockSize
+ currentTheme?.useDark = settings.defaultLN.useDarkTheme
currentTheme?.let { binding.bookReader.setAppearance(it) }
- if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ if (settings.defaultLN.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt
index 27a9caaae93..760c7eb3d82 100644
--- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt
@@ -30,8 +30,7 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as NovelReaderActivity
- val settings = activity.settings.default
-
+ val settings = activity.settings.defaultLN
val themeLabels = activity.themes.map { it.name }
binding.themeSelect.adapter =
NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels)
@@ -49,7 +48,11 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
-
+ binding.useOledTheme.isChecked = settings.useOledTheme
+ binding.useOledTheme.setOnCheckedChangeListener { _, isChecked ->
+ settings.useOledTheme = isChecked
+ activity.applySettings()
+ }
val layoutList = listOf(
binding.paged,
binding.continuous
@@ -173,6 +176,20 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
binding.maxBlockSize.setText(value.toString())
activity.applySettings()
}
+
+ }
+ binding.incrementMaxBlockSize.setOnClickListener {
+ val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
+ settings.maxBlockSize = value + 10
+ binding.maxBlockSize.setText(settings.maxBlockSize.toString())
+ activity.applySettings()
+ }
+
+ binding.decrementMaxBlockSize.setOnClickListener {
+ val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
+ settings.maxBlockSize = value - 10
+ binding.maxBlockSize.setText(settings.maxBlockSize.toString())
+ activity.applySettings()
}
binding.useDarkTheme.isChecked = settings.useDarkTheme
diff --git a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt
index ada753a478e..a6d044e18f0 100644
--- a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt
@@ -1,23 +1,29 @@
package ani.dantotsu.media.user
import android.annotation.SuppressLint
+import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.View
+import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
+import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
+import ani.dantotsu.currContext
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData
+import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.settings.UserInterfaceSettings
+import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -74,13 +80,16 @@ class ListActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
+ binding.settingsContainer.updateLayoutParams {
+ topMargin = statusBarHeight
+ bottomMargin = navBarHeight
+ }
}
setContentView(binding.root)
val anime = intent.getBooleanExtra("anime", true)
binding.listTitle.text =
intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
-
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
this@ListActivity.selectedTabIdx = tab?.position ?: 0
@@ -145,7 +154,8 @@ class ListActivity : AppCompatActivity() {
R.id.release -> "release"
else -> null
}
-
+ currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
+ ?.putString("sort_order", sort)?.apply()
binding.listProgressBar.visibility = View.VISIBLE
binding.listViewPager.adapter = null
scope.launch {
diff --git a/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt b/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt
index 3eccf001a22..48573e5064d 100644
--- a/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt
@@ -46,7 +46,7 @@ class ListFragment : Fragment() {
binding.listRecyclerView.layoutManager =
GridLayoutManager(
requireContext(),
- if (grid!!) (screenWidth / 124f).toInt() else 1
+ if (grid!!) (screenWidth / 120f).toInt() else 1
)
binding.listRecyclerView.adapter = adapter
}
diff --git a/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt b/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt
index fb780651a1c..02779ab28d5 100644
--- a/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt
+++ b/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt
@@ -1,11 +1,13 @@
package ani.dantotsu.offline
+import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
+import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentOfflineBinding
import ani.dantotsu.isOnline
import ani.dantotsu.navBarHeight
@@ -13,6 +15,7 @@ import ani.dantotsu.startMainActivity
import ani.dantotsu.statusBarHeight
class OfflineFragment : Fragment() {
+ private var offline = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -23,6 +26,11 @@ class OfflineFragment : Fragment() {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
+ offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getBoolean("offlineMode", false) ?: false
+ binding.noInternet.text =
+ if (offline) "Offline Mode" else getString(R.string.no_internet)
+ binding.refreshButton.visibility = if (offline) View.GONE else View.VISIBLE
binding.refreshButton.setOnClickListener {
if (isOnline(requireContext())) {
startMainActivity(requireActivity())
@@ -30,4 +38,10 @@ class OfflineFragment : Fragment() {
}
return binding.root
}
+
+ override fun onResume() {
+ super.onResume()
+ offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ ?.getBoolean("offlineMode", false) ?: false
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/others/DisabledReports.kt b/app/src/main/java/ani/dantotsu/others/DisabledReports.kt
index 8dc4a0e46a9..ed8f5813f94 100644
--- a/app/src/main/java/ani/dantotsu/others/DisabledReports.kt
+++ b/app/src/main/java/ani/dantotsu/others/DisabledReports.kt
@@ -1,4 +1,5 @@
package ani.dantotsu.others
const val DisabledReports = false
-//Setting this to false, will allow sending crash reports to Dantotsu's Firebase Crashlytics
\ No newline at end of file
+//Setting this to false, will allow sending crash reports to Dantotsu's Firebase Crashlytics
+//if you want a custom build without crash reporting, set this to true
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/others/Kitsu.kt b/app/src/main/java/ani/dantotsu/others/Kitsu.kt
index e815572909b..bee4beeea78 100644
--- a/app/src/main/java/ani/dantotsu/others/Kitsu.kt
+++ b/app/src/main/java/ani/dantotsu/others/Kitsu.kt
@@ -6,26 +6,38 @@ import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.tryWithSuspend
+import com.google.gson.Gson
+import com.lagradost.nicehttp.NiceResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import java.io.InputStreamReader
+import java.util.zip.GZIPInputStream
object Kitsu {
private suspend fun getKitsuData(query: String): KitsuResponse? {
val headers = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json",
+ "Accept-Encoding" to "gzip, deflate",
+ "Accept-Language" to "en-US,en;q=0.5",
+ "Host" to "kitsu.io",
"Connection" to "keep-alive",
- "DNT" to "1",
- "Origin" to "https://kitsu.io"
+ "Origin" to "https://kitsu.io",
+ "Sec-Fetch-Dest" to "empty",
+ "Sec-Fetch-Mode" to "cors",
+ "Sec-Fetch-Site" to "cross-site",
)
- val json = tryWithSuspend {
- client.post(
+ val response = tryWithSuspend {
+ val res = client.post(
"https://kitsu.io/api/graphql",
headers,
data = mapOf("query" to query)
)
+ res
}
- return json?.parsed()
+ val json = decodeToString(response)
+ val gson = Gson()
+ return gson.fromJson(json, KitsuResponse::class.java)
}
suspend fun getKitsuEpisodesDetails(media: Media): Map? {
@@ -54,14 +66,14 @@ query {
}
}
}
-}"""
+}""".trimIndent()
val result = getKitsuData(query) ?: return null
logger("Kitsu : result=$result", print)
media.idKitsu = result.data?.lookupMapping?.id
- return (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
- val num = ep?.num?.toString() ?: return@mapNotNull null
+ val a = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
+ val num = ep?.number?.toString() ?: return@mapNotNull null
num to Episode(
number = num,
title = ep.titles?.canonical,
@@ -69,6 +81,25 @@ query {
thumb = FileUrl[ep.thumbnail?.original?.url],
)
}.toMap()
+ logger("Kitsu : a=$a", print)
+ return a
+ }
+
+ private fun decodeToString(res: NiceResponse?): String? {
+ return when (res?.headers?.get("Content-Encoding")) {
+ "gzip" -> {
+ res.body.byteStream().use { inputStream ->
+ GZIPInputStream(inputStream).use { gzipInputStream ->
+ InputStreamReader(gzipInputStream).use { reader ->
+ reader.readText()
+ }
+ }
+ }
+ }
+ else -> {
+ res?.body?.string()
+ }
+ }
}
@Serializable
@@ -93,7 +124,7 @@ query {
@Serializable
data class Node(
- @SerialName("number") val num: Long? = null,
+ @SerialName("number") val number: Int? = null,
@SerialName("titles") val titles: Titles? = null,
@SerialName("description") val description: Description? = null,
@SerialName("thumbnail") val thumbnail: Thumbnail? = null
diff --git a/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt b/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt
index 25f8f6a4816..3d9c439bb5c 100644
--- a/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt
+++ b/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt
@@ -6,25 +6,116 @@ class LanguageMapper {
fun mapLanguageCodeToName(code: String): String {
return when (code) {
"all" -> "Multi"
+ "af" -> "Afrikaans"
+ "am" -> "Amharic"
"ar" -> "Arabic"
+ "as" -> "Assamese"
+ "az" -> "Azerbaijani"
+ "be" -> "Belarusian"
+ "bg" -> "Bulgarian"
+ "bn" -> "Bengali"
+ "bs" -> "Bosnian"
+ "ca" -> "Catalan"
+ "ceb" -> "Cebuano"
+ "cs" -> "Czech"
+ "da" -> "Danish"
"de" -> "German"
+ "el" -> "Greek"
"en" -> "English"
+ "en-Us" -> "English (United States)"
+ "eo" -> "Esperanto"
"es" -> "Spanish"
+ "es-419" -> "Spanish (Latin America)"
+ "et" -> "Estonian"
+ "eu" -> "Basque"
+ "fa" -> "Persian"
+ "fi" -> "Finnish"
+ "fil" -> "Filipino"
+ "fo" -> "Faroese"
"fr" -> "French"
+ "ga" -> "Irish"
+ "gn" -> "Guarani"
+ "gu" -> "Gujarati"
+ "ha" -> "Hausa"
+ "he" -> "Hebrew"
+ "hi" -> "Hindi"
+ "hr" -> "Croatian"
+ "ht" -> "Haitian Creole"
+ "hu" -> "Hungarian"
+ "hy" -> "Armenian"
"id" -> "Indonesian"
+ "ig" -> "Igbo"
+ "is" -> "Icelandic"
"it" -> "Italian"
"ja" -> "Japanese"
+ "jv" -> "Javanese"
+ "ka" -> "Georgian"
+ "kk" -> "Kazakh"
+ "km" -> "Khmer"
+ "kn" -> "Kannada"
"ko" -> "Korean"
+ "ku" -> "Kurdish"
+ "ky" -> "Kyrgyz"
+ "la" -> "Latin"
+ "lb" -> "Luxembourgish"
+ "lo" -> "Lao"
+ "lt" -> "Lithuanian"
+ "lv" -> "Latvian"
+ "mg" -> "Malagasy"
+ "mi" -> "Maori"
+ "mk" -> "Macedonian"
+ "ml" -> "Malayalam"
+ "mn" -> "Mongolian"
+ "mo" -> "Moldovan"
+ "mr" -> "Marathi"
+ "ms" -> "Malay"
+ "mt" -> "Maltese"
+ "my" -> "Burmese"
+ "ne" -> "Nepali"
+ "nl" -> "Dutch"
+ "no" -> "Norwegian"
+ "ny" -> "Chichewa"
"pl" -> "Polish"
+ "pt" -> "Portuguese"
"pt-BR" -> "Portuguese (Brazil)"
+ "pt-PT" -> "Portuguese (Portugal)"
+ "ps" -> "Pashto"
+ "ro" -> "Romanian"
+ "rm" -> "Romansh"
"ru" -> "Russian"
+ "sd" -> "Sindhi"
+ "sh" -> "Serbo-Croatian"
+ "si" -> "Sinhala"
+ "sk" -> "Slovak"
+ "sl" -> "Slovenian"
+ "sm" -> "Samoan"
+ "sn" -> "Shona"
+ "so" -> "Somali"
+ "sq" -> "Albanian"
+ "sr" -> "Serbian"
+ "st" -> "Southern Sotho"
+ "sv" -> "Swedish"
+ "sw" -> "Swahili"
+ "ta" -> "Tamil"
+ "te" -> "Telugu"
+ "tg" -> "Tajik"
"th" -> "Thai"
+ "ti" -> "Tigrinya"
+ "tk" -> "Turkmen"
+ "tl" -> "Tagalog"
+ "to" -> "Tongan"
"tr" -> "Turkish"
"uk" -> "Ukrainian"
+ "ur" -> "Urdu"
+ "uz" -> "Uzbek"
"vi" -> "Vietnamese"
+ "yo" -> "Yoruba"
"zh" -> "Chinese"
"zh-Hans" -> "Chinese (Simplified)"
- else -> ""
+ "zh-Hant" -> "Chinese (Traditional)"
+ "zh-Habt" -> "Chinese (Hakka)"
+ "zu" -> "Zulu"
+ else -> code
}
}
diff --git a/app/src/main/java/ani/dantotsu/others/MalScraper.kt b/app/src/main/java/ani/dantotsu/others/MalScraper.kt
index 8b39ac64401..648f3a4a312 100644
--- a/app/src/main/java/ani/dantotsu/others/MalScraper.kt
+++ b/app/src/main/java/ani/dantotsu/others/MalScraper.kt
@@ -50,7 +50,7 @@ object MalScraper {
}
}
} catch (e: Exception) {
- if (e is TimeoutCancellationException) snackString(currContext()?.getString(R.string.error_loading_mal_data))
+ // if (e is TimeoutCancellationException) snackString(currContext()?.getString(R.string.error_loading_mal_data))
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/ani/dantotsu/others/SharedPreferenceLiveData.kt b/app/src/main/java/ani/dantotsu/others/SharedPreferenceLiveData.kt
new file mode 100644
index 00000000000..70c9b4f75f4
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/others/SharedPreferenceLiveData.kt
@@ -0,0 +1,112 @@
+package ani.dantotsu.others
+
+import android.content.SharedPreferences
+import androidx.lifecycle.LiveData
+
+abstract class SharedPreferenceLiveData(
+ val sharedPrefs: SharedPreferences,
+ val key: String,
+ val defValue: T
+) : LiveData() {
+
+ private val preferenceChangeListener =
+ SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
+ if (key == this.key) {
+ value = getValueFromPreferences(key, defValue)
+ }
+ }
+
+ abstract fun getValueFromPreferences(key: String, defValue: T): T
+
+ override fun onActive() {
+ super.onActive()
+ value = getValueFromPreferences(key, defValue)
+ sharedPrefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
+ }
+
+ override fun onInactive() {
+ sharedPrefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
+ super.onInactive()
+ }
+}
+
+class SharedPreferenceIntLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Int) :
+ SharedPreferenceLiveData(sharedPrefs, key, defValue) {
+ override fun getValueFromPreferences(key: String, defValue: Int): Int =
+ sharedPrefs.getInt(key, defValue)
+}
+
+class SharedPreferenceStringLiveData(
+ sharedPrefs: SharedPreferences,
+ key: String,
+ defValue: String
+) :
+ SharedPreferenceLiveData(sharedPrefs, key, defValue) {
+ override fun getValueFromPreferences(key: String, defValue: String): String =
+ sharedPrefs.getString(key, defValue).toString()
+}
+
+class SharedPreferenceBooleanLiveData(
+ sharedPrefs: SharedPreferences,
+ key: String,
+ defValue: Boolean
+) :
+ SharedPreferenceLiveData(sharedPrefs, key, defValue) {
+ override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean =
+ sharedPrefs.getBoolean(key, defValue)
+}
+
+class SharedPreferenceFloatLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Float) :
+ SharedPreferenceLiveData(sharedPrefs, key, defValue) {
+ override fun getValueFromPreferences(key: String, defValue: Float): Float =
+ sharedPrefs.getFloat(key, defValue)
+}
+
+class SharedPreferenceLongLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Long) :
+ SharedPreferenceLiveData(sharedPrefs, key, defValue) {
+ override fun getValueFromPreferences(key: String, defValue: Long): Long =
+ sharedPrefs.getLong(key, defValue)
+}
+
+class SharedPreferenceStringSetLiveData(
+ sharedPrefs: SharedPreferences,
+ key: String,
+ defValue: Set
+) :
+ SharedPreferenceLiveData>(sharedPrefs, key, defValue) {
+ override fun getValueFromPreferences(key: String, defValue: Set): Set =
+ sharedPrefs.getStringSet(key, defValue)?.toSet() ?: defValue
+}
+
+fun SharedPreferences.intLiveData(key: String, defValue: Int): SharedPreferenceLiveData {
+ return SharedPreferenceIntLiveData(this, key, defValue)
+}
+
+fun SharedPreferences.stringLiveData(
+ key: String,
+ defValue: String
+): SharedPreferenceLiveData {
+ return SharedPreferenceStringLiveData(this, key, defValue)
+}
+
+fun SharedPreferences.booleanLiveData(
+ key: String,
+ defValue: Boolean
+): SharedPreferenceLiveData {
+ return SharedPreferenceBooleanLiveData(this, key, defValue)
+}
+
+fun SharedPreferences.floatLiveData(key: String, defValue: Float): SharedPreferenceLiveData {
+ return SharedPreferenceFloatLiveData(this, key, defValue)
+}
+
+fun SharedPreferences.longLiveData(key: String, defValue: Long): SharedPreferenceLiveData {
+ return SharedPreferenceLongLiveData(this, key, defValue)
+}
+
+fun SharedPreferences.stringSetLiveData(
+ key: String,
+ defValue: Set
+): SharedPreferenceLiveData> {
+ return SharedPreferenceStringSetLiveData(this, key, defValue)
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt
index 1e38fc0e8d5..282c17fc40d 100644
--- a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt
+++ b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt
@@ -4,9 +4,11 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
+import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -14,7 +16,9 @@ import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ActivityImageSearchBinding
+import ani.dantotsu.initActivity
import ani.dantotsu.media.MediaDetailsActivity
+import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
@@ -49,10 +53,13 @@ class ImageSearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this)
+ initActivity(this)
ThemeManager(this).applyTheme()
binding = ActivityImageSearchBinding.inflate(layoutInflater)
setContentView(binding.root)
-
+ binding.uploadImage.updateLayoutParams {
+ bottomMargin = navBarHeight
+ }
binding.uploadImage.setOnClickListener {
viewModel.clearResults()
imageSelectionLauncher.launch("image/*")
diff --git a/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt b/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt
new file mode 100644
index 00000000000..d1f3635e1e2
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt
@@ -0,0 +1,60 @@
+package ani.dantotsu.others.webview
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.os.Build
+import android.os.Bundle
+import android.webkit.CookieManager
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.appcompat.app.AppCompatActivity
+import ani.dantotsu.R
+import ani.dantotsu.themes.ThemeManager
+import eu.kanade.tachiyomi.network.NetworkHelper
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class CookieCatcher : AppCompatActivity() {
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ ThemeManager(this).applyTheme()
+
+ //get url from intent
+ val url = intent.getStringExtra("url") ?: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ val process = Application.getProcessName()
+ if (packageName != process) WebView.setDataDirectorySuffix(process)
+ }
+ setContentView(R.layout.activity_discord)
+
+ val webView = findViewById(R.id.discordWebview)
+
+ val cookies: CookieManager = Injekt.get().cookieJar.manager
+ cookies.setAcceptThirdPartyCookies(webView, true)
+
+ webView.apply {
+ settings.javaScriptEnabled = true
+ settings.databaseEnabled = true
+ settings.domStorageEnabled = true
+ }
+ WebView.setWebContentsDebuggingEnabled(true)
+ webView.webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(
+ view: WebView?,
+ request: WebResourceRequest?
+ ): Boolean {
+ return super.shouldOverrideUrlLoading(view, request)
+ }
+
+ override fun onPageFinished(view: WebView?, url: String?) {
+ super.onPageFinished(view, url)
+ }
+ }
+
+ webView.loadUrl(url)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/others/webview/WebViewBottomDialog.kt b/app/src/main/java/ani/dantotsu/others/webview/WebViewBottomDialog.kt
index 257a703cc38..489c68fc801 100644
--- a/app/src/main/java/ani/dantotsu/others/webview/WebViewBottomDialog.kt
+++ b/app/src/main/java/ani/dantotsu/others/webview/WebViewBottomDialog.kt
@@ -11,6 +11,9 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.FileUrl
import ani.dantotsu.databinding.BottomSheetWebviewBinding
import ani.dantotsu.defaultHeaders
+import eu.kanade.tachiyomi.network.NetworkHelper
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
@@ -30,7 +33,8 @@ abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
dismiss()
}
- val cookies: CookieManager = CookieManager.getInstance()
+ val cookies: CookieManager = Injekt.get().cookieJar.manager
+ //CookieManager.getInstance()
override fun onCreateView(
inflater: LayoutInflater,
diff --git a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
index c48a4b3e1ed..11a69c3101d 100644
--- a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
@@ -1,5 +1,6 @@
package ani.dantotsu.parsers
+import android.content.Context
import ani.dantotsu.Lazier
import ani.dantotsu.lazyList
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@@ -8,24 +9,52 @@ import kotlinx.coroutines.flow.first
object AnimeSources : WatchSources() {
override var list: List> = emptyList()
+ var pinnedAnimeSources: Set = emptySet()
+
+ suspend fun init(fromExtensions: StateFlow>, context: Context) {
+ val sharedPrefs = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ pinnedAnimeSources = sharedPrefs.getStringSet("pinned_anime_sources", emptySet()) ?: emptySet()
- suspend fun init(fromExtensions: StateFlow>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
- list = createParsersFromExtensions(initialExtensions)
+ list = createParsersFromExtensions(initialExtensions) + Lazier(
+ { OfflineAnimeParser() },
+ "Downloaded"
+ )
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
- list = createParsersFromExtensions(extensions)
+ list = sortPinnedAnimeSources(createParsersFromExtensions(extensions), pinnedAnimeSources) + Lazier(
+ { OfflineAnimeParser() },
+ "Downloaded"
+ )
}
}
+ fun performReorderAnimeSources() {
+ //remove the downloaded source from the list to avoid duplicates
+ list = list.filter { it.name != "Downloaded" }
+ list = sortPinnedAnimeSources(list, pinnedAnimeSources) + Lazier(
+ { OfflineAnimeParser() },
+ "Downloaded"
+ )
+ }
+
private fun createParsersFromExtensions(extensions: List): List> {
return extensions.map { extension ->
val name = extension.name
Lazier({ DynamicAnimeParser(extension) }, name)
}
}
+
+ private fun sortPinnedAnimeSources(Sources: List>, pinnedAnimeSources: Set): List> {
+ //find the pinned sources
+ val pinnedSources = Sources.filter { pinnedAnimeSources.contains(it.name) }
+ //find the unpinned sources
+ val unpinnedSources = Sources.filter { !pinnedAnimeSources.contains(it.name) }
+ //put the pinned sources at the top of the list
+ return pinnedSources + unpinnedSources
+ }
}
diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
index 03b40919051..4ef6aee20cf 100644
--- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
@@ -9,23 +9,22 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
-import android.widget.Toast
import ani.dantotsu.FileUrl
-import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.media.anime.AnimeNameAdapter
import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.snackString
-import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
+import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
+import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@@ -40,7 +39,10 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.sync.Semaphore
+import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
+import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
@@ -81,50 +83,56 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
- }
- if (source is AnimeCatalogueSource) {
- try {
- val res = source.getEpisodeList(sAnime)
-
- val sortedEpisodes = if (res[0].episode_number == -1f) {
- // Find the number in the string and sort by that number
- val sortedByStringNumber = res.sortedBy {
- val matchResult = "\\d+".toRegex().find(it.name)
- val number = matchResult?.value?.toFloat() ?: Float.MAX_VALUE
- it.episode_number = number // Store the found number in episode_number
- number
- }
+ } as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
+ ?: return emptyList())
+ try {
+ val res = source.getEpisodeList(sAnime)
+
+ val sortedEpisodes = if (res[0].episode_number == -1f) {
+ // Find the number in the string and sort by that number
+ val sortedByStringNumber = res.sortedBy {
+ val matchResult = AnimeNameAdapter.findEpisodeNumber(it.name)
+ val number = matchResult ?: Float.MAX_VALUE
+ it.episode_number = number // Store the found number in episode_number
+ number
+ }
- // If there is no number, reverse the order and give them an incrementing number
- var incrementingNumber = 1f
- sortedByStringNumber.map {
- if (it.episode_number == Float.MAX_VALUE) {
- it.episode_number =
- incrementingNumber++ // Update episode_number with the incrementing number
- }
- it
+ // If there is no number, reverse the order and give them an incrementing number
+ var incrementingNumber = 1f
+ sortedByStringNumber.map {
+ if (it.episode_number == Float.MAX_VALUE) {
+ it.episode_number =
+ incrementingNumber++ // Update episode_number with the incrementing number
}
- } else {
- var episodeCounter = 1f
- // Group by season, sort within each season, and then renumber while keeping episode number 0 as is
- val seasonGroups =
- res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
- seasonGroups.keys.sorted().flatMap { season ->
- seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
- if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
- episode.episode_number = episodeCounter++
+ it
+ }
+ } else {
+ var episodeCounter = 1f
+ // Group by season, sort within each season, and then renumber while keeping episode number 0 as is
+ val seasonGroups =
+ res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
+ seasonGroups.keys.sortedBy { it.toInt() }
+ .flatMap { season ->
+ seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
+ if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
+ val potentialNumber =
+ AnimeNameAdapter.findEpisodeNumber(episode.name)
+ if (potentialNumber != null) {
+ episode.episode_number = potentialNumber
+ } else {
+ episode.episode_number = episodeCounter
}
- episode
- } ?: emptyList()
- }
+ episodeCounter++
+ }
+ episode
+ } ?: emptyList()
}
- return sortedEpisodes.map { SEpisodeToEpisode(it) }
- } catch (e: Exception) {
- println("Exception: $e")
}
- return emptyList()
+ return sortedEpisodes.map { SEpisodeToEpisode(it) }
+ } catch (e: Exception) {
+ logger("Exception: $e")
}
- return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
+ return emptyList()
}
override suspend fun loadVideoServers(
@@ -137,7 +145,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
- } as? AnimeCatalogueSource ?: return emptyList()
+ } as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
+ ?: return emptyList())
return try {
val videos = source.getVideoList(sEpisode)
@@ -159,15 +168,16 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
- } as? AnimeCatalogueSource ?: return emptyList()
+ } as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
+ ?: return emptyList())
return try {
val res = source.fetchSearchAnime(1, query, source.getFilterList()).awaitSingle()
+ logger("query: $query")
convertAnimesPageToShowResponse(res)
} catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
withContext(Dispatchers.Main) {
- Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
- .show()
+ snackString( "Failed to bypass Cloudflare")
}
emptyList()
} catch (e: Exception) {
@@ -202,7 +212,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
}
return Episode(
if (episodeNumberInt.toInt() != -1) {
- episodeNumberInt.toString()
+ if (sEpisode.episode_number % 1 == 0f) {
+ episodeNumberInt.toInt().toString()
+ } else {
+ sEpisode.episode_number.toString()
+ }
} else {
sEpisode.name
},
@@ -277,7 +291,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
var imageDataList: List = listOf()
val ret = coroutineScope {
try {
- println("source.name " + source.name)
+ logger("source.name " + source.name)
val res = source.getPageList(sChapter)
val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
@@ -295,8 +309,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} catch (e: Exception) {
logger("loadImages Exception: $e")
- Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
- .show()
+ snackString("Failed to load images: $e")
emptyList()
}
}
@@ -310,29 +323,30 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
- var imageDataList: List = listOf()
- coroutineScope {
+
+ return coroutineScope {
try {
- println("source.name " + source.name)
+ logger("source.name " + source.name)
val res = source.getPageList(sChapter)
val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
+ val semaphore = Semaphore(5)
val deferreds = reIndexedPages.map { page ->
async(Dispatchers.IO) {
- imageDataList += ImageData(page, source)
+ semaphore.withPermit {
+ ImageData(page, source)
+ }
}
}
deferreds.awaitAll()
-
} catch (e: Exception) {
logger("loadImages Exception: $e")
snackString("Failed to load images: $e")
emptyList()
}
}
- return imageDataList
}
suspend fun fetchAndProcessImage(
@@ -344,8 +358,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
try {
// Fetch the image
val response = httpSource.getImage(page)
- println("Response: ${response.code}")
- println("Response: ${response.message}")
+ logger("Response: ${response.code}")
+ logger("Response: ${response.message}")
// Convert the Response to an InputStream
val inputStream = response.body.byteStream()
@@ -353,7 +367,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
- inputStream?.close()
+ inputStream.close()
ani.dantotsu.media.manga.saveImage(
bitmap,
context.contentResolver,
@@ -365,7 +379,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
return@withContext bitmap
} catch (e: Exception) {
// Handle any exceptions
- println("An error occurred: ${e.message}")
+ logger("An error occurred: ${e.message}")
return@withContext null
}
}
@@ -395,10 +409,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
)
}
- inputStream?.close()
+ inputStream.close()
} catch (e: Exception) {
// Handle any exceptions
- println("An error occurred: ${e.message}")
+ logger("An error occurred: ${e.message}")
}
}
}
@@ -445,7 +459,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
} catch (e: Exception) {
// Handle exception here
- println("Exception while saving image: ${e.message}")
+ logger("Exception while saving image: ${e.message}")
}
}
@@ -465,8 +479,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
withContext(Dispatchers.Main) {
- Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
- .show()
+ snackString("Failed to bypass Cloudflare")
}
emptyList()
} catch (e: Exception) {
@@ -603,13 +616,18 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = getVideoType(fileName)
+ // this solves a problem no one has, so I'm commenting it out for now
+ //if (format == null) {
+ // val networkHelper = Injekt.get()
+ // format = headRequest(videoUrl, networkHelper)
+ //}
}
- // If the format is still undetermined, log an error or handle it appropriately
+ // If the format is still undetermined, log an error
if (format == null) {
logger("Unknown video format: $videoUrl")
- FirebaseCrashlytics.getInstance()
- .recordException(Exception("Unknown video format: $videoUrl"))
+ //FirebaseCrashlytics.getInstance()
+ // .recordException(Exception("Unknown video format: $videoUrl"))
format = VideoType.CONTAINER
}
val headersMap: Map =
@@ -620,12 +638,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
number,
format,
FileUrl(videoUrl, headersMap),
- aniVideo.totalContentLength.toDouble()
+ if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
)
}
private fun getVideoType(fileName: String): VideoType? {
- return when {
+ val type = when {
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
".mkv",
ignoreCase = true
@@ -635,6 +653,47 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> null
}
+
+ return type
+ }
+
+ private fun headRequest(fileName: String, networkHelper: NetworkHelper): VideoType? {
+ return try {
+ logger("attempting head request for $fileName")
+ val request = Request.Builder()
+ .url(fileName)
+ .head()
+ .build()
+
+ networkHelper.client.newCall(request).execute().use { response ->
+ val contentType = response.header("Content-Type")
+ val contentDisposition = response.header("Content-Disposition")
+
+ if (contentType != null) {
+ when {
+ contentType.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
+ contentType.contains("dash", ignoreCase = true) -> VideoType.DASH
+ contentType.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
+ else -> null
+ }
+ } else if (contentDisposition != null) {
+ when {
+ contentDisposition.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
+ contentDisposition.contains("dash", ignoreCase = true) -> VideoType.DASH
+ contentDisposition.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
+ else -> null
+ }
+ } else {
+ logger("failed head request for $fileName")
+ null
+ }
+
+ }
+ } catch (e: Exception) {
+ logger("Exception in headRequest: $e")
+ null
+ }
+
}
private fun TrackToSubtitle(track: Track): Subtitle {
@@ -646,7 +705,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
}
- private fun findSubtitleType(url: String): SubtitleType? {
+ private fun findSubtitleType(url: String): SubtitleType {
// First, try to determine the type based on the URL file extension
val type: SubtitleType = when {
url.endsWith(".vtt", true) -> SubtitleType.VTT
diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
index 03562f68a2e..40ce4601ea8 100644
--- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
@@ -56,7 +56,7 @@ abstract class BaseParser {
* **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response: ShowResponse? = loadSavedShowResponse(mediaObj.id)
- if (response != null && this !is OfflineMangaParser) {
+ if (response != null && this !is OfflineMangaParser && this !is OfflineAnimeParser) {
saveShowResponse(mediaObj.id, response, true)
} else {
setUserText("Searching : ${mediaObj.mainName()}")
@@ -156,9 +156,9 @@ abstract class BaseParser {
}
fun checkIfVariablesAreEmpty() {
- if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `hostUrl` for the Parser")
- if (name.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `name` for the Parser")
- if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `saveName` for the Parser")
+ if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
+ if (name.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
+ if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
}
open var showUserText = ""
diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt
index f8535772cf1..5aa6ffc24e2 100644
--- a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt
@@ -16,6 +16,9 @@ abstract class WatchSources : BaseSources() {
?: EmptyAnimeParser()
}
+ fun isDownloadedSource(i: Int): Boolean {
+ return get(i) is OfflineAnimeParser
+ }
suspend fun loadEpisodesFromMedia(i: Int, media: Media): MutableMap {
return tryWithSuspend(true) {
@@ -46,6 +49,19 @@ abstract class WatchSources : BaseSources() {
sEpisode = it.sEpisode
)
}
+ } else if (parser is OfflineAnimeParser) {
+ parser.loadEpisodes(showLink, extra, SAnime.create()).forEach {
+ map[it.number] = Episode(
+ it.number,
+ it.link,
+ it.title,
+ it.description,
+ it.thumbnail,
+ it.isFiller,
+ extra = it.extra,
+ sEpisode = it.sEpisode
+ )
+ }
}
}
return map
diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt
index 0f8a5642e81..a6da1540bc1 100644
--- a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt
@@ -1,5 +1,6 @@
package ani.dantotsu.parsers
+import android.content.Context
import ani.dantotsu.Lazier
import ani.dantotsu.lazyList
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
@@ -7,12 +8,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() {
- // Instantiate the static parser
- private val offlineMangaParser by lazy { OfflineMangaParser() }
-
override var list: List> = emptyList()
+ var pinnedMangaSources: Set = emptySet()
+
+ suspend fun init(fromExtensions: StateFlow>, context: Context) {
+ val sharedPrefs = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
+ pinnedMangaSources = sharedPrefs.getStringSet("pinned_manga_sources", emptySet()) ?: emptySet()
- suspend fun init(fromExtensions: StateFlow>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions) + Lazier(
@@ -22,19 +24,37 @@ object MangaSources : MangaReadSources() {
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
- list = createParsersFromExtensions(extensions) + Lazier(
+ list = sortPinnedMangaSources(createParsersFromExtensions(extensions), pinnedMangaSources) + Lazier(
{ OfflineMangaParser() },
"Downloaded"
)
}
}
+ fun performReorderMangaSources() {
+ //remove the downloaded source from the list to avoid duplicates
+ list = list.filter { it.name != "Downloaded" }
+ list = sortPinnedMangaSources(list, pinnedMangaSources) + Lazier(
+ { OfflineMangaParser() },
+ "Downloaded"
+ )
+ }
+
private fun createParsersFromExtensions(extensions: List): List> {
return extensions.map { extension ->
val name = extension.name
Lazier({ DynamicMangaParser(extension) }, name)
}
}
+
+ private fun sortPinnedMangaSources(Sources: List>, pinnedMangaSources: Set): List> {
+ //find the pinned sources
+ val pinnedSources = Sources.filter { pinnedMangaSources.contains(it.name) }
+ //find the unpinned sources
+ val unpinnedSources = Sources.filter { !pinnedMangaSources.contains(it.name) }
+ //put the pinned sources at the top of the list
+ return pinnedSources + unpinnedSources
+ }
}
object HMangaSources : MangaReadSources() {
diff --git a/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt
index 9a625600d21..f2bc0703257 100644
--- a/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt
@@ -34,7 +34,7 @@ abstract class NovelParser : BaseParser() {
//val query = mediaObj.name ?: mediaObj.nameRomaji
//return search(query).sortByVolume(query)
val results: List
- return if(mediaObj.name != null) {
+ return if (mediaObj.name != null) {
val query = mediaObj.name
results = search(query).sortByVolume(query)
results.ifEmpty {
diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt
new file mode 100644
index 00000000000..0f8c79243ee
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt
@@ -0,0 +1,161 @@
+package ani.dantotsu.parsers
+
+import android.net.Uri
+import android.os.Environment
+import ani.dantotsu.currContext
+import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.media.anime.AnimeNameAdapter
+import ani.dantotsu.tryWithSuspend
+import eu.kanade.tachiyomi.animesource.model.SAnime
+import eu.kanade.tachiyomi.animesource.model.SEpisode
+import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
+import me.xdrop.fuzzywuzzy.FuzzySearch
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.File
+import java.util.Locale
+
+class OfflineAnimeParser : AnimeParser() {
+ private val downloadManager = Injekt.get()
+
+ override val name = "Offline"
+ override val saveName = "Offline"
+ override val hostUrl = "Offline"
+ override val isDubAvailableSeparately = false
+ override val isNSFW = false
+
+ override suspend fun loadEpisodes(
+ animeLink: String,
+ extra: Map?,
+ sAnime: SAnime
+ ): List {
+ val directory = File(
+ currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
+ "${DownloadsManager.animeLocation}/$animeLink"
+ )
+ //get all of the folder names and add them to the list
+ val episodes = mutableListOf()
+ if (directory.exists()) {
+ directory.listFiles()?.forEach {
+ //put the title and episdode number in the extra data
+ val extraData = mutableMapOf()
+ extraData["title"] = animeLink
+ extraData["episode"] = it.name
+ if (it.isDirectory) {
+ val episode = Episode(
+ it.name,
+ "$animeLink - ${it.name}",
+ it.name,
+ null,
+ null,
+ extra = extraData,
+ sEpisode = SEpisodeImpl()
+ )
+ episodes.add(episode)
+ }
+ }
+ episodes.sortBy { AnimeNameAdapter.findEpisodeNumber(it.number) }
+ return episodes
+ }
+ return emptyList()
+ }
+
+ override suspend fun loadVideoServers(
+ episodeLink: String,
+ extra: Map?,
+ sEpisode: SEpisode
+ ): List {
+ return listOf(
+ VideoServer(
+ episodeLink,
+ offline = true,
+ extraData = extra
+ )
+ )
+ }
+
+
+ override suspend fun search(query: String): List {
+ val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
+ val returnTitles: MutableList = mutableListOf()
+ for (title in titles) {
+ if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
+ returnTitles.add(title)
+ }
+ }
+ val returnList: MutableList = mutableListOf()
+ for (title in returnTitles) {
+ returnList.add(ShowResponse(title, title, title))
+ }
+ return returnList
+ }
+
+ override suspend fun loadByVideoServers(
+ episodeUrl: String,
+ extra: Map?,
+ sEpisode: SEpisode,
+ callback: (VideoExtractor) -> Unit
+ ) {
+ val server = loadVideoServers(episodeUrl, extra, sEpisode).first()
+ OfflineVideoExtractor(server).apply {
+ tryWithSuspend {
+ load()
+ }
+ callback.invoke(this)
+ }
+ }
+
+ override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor {
+ return OfflineVideoExtractor(server)
+ }
+
+}
+
+class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
+ override val server: VideoServer
+ get() = videoServer
+
+ override suspend fun extract(): VideoContainer {
+ val sublist = getSubtitle(
+ videoServer.extraData?.get("title") ?: "",
+ videoServer.extraData?.get("episode") ?: ""
+ )?: emptyList()
+ //we need to return a "fake" video so that the app doesn't crash
+ val video = Video(
+ null,
+ VideoType.CONTAINER,
+ "",
+ )
+ return VideoContainer(listOf(video), sublist)
+ }
+
+ private fun getSubtitle(title: String, episode: String): List? {
+ currContext()?.let {
+ DownloadsManager.getDirectory(
+ it,
+ ani.dantotsu.download.DownloadedType.Type.ANIME,
+ title,
+ episode
+ ).listFiles()?.forEach {
+ if (it.name.contains("subtitle")) {
+ return listOf(
+ Subtitle(
+ "Downloaded Subtitle",
+ Uri.fromFile(it).toString(),
+ determineSubtitletype(it.absolutePath)
+ )
+ )
+ }
+ }
+ }
+ return null
+ }
+
+ fun determineSubtitletype(url: String): SubtitleType {
+ return when {
+ url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
+ url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
+ else -> SubtitleType.SRT
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
index 218a0f9aada..deffb420b88 100644
--- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
@@ -76,7 +76,7 @@ class OfflineMangaParser : MangaParser() {
}
override suspend fun search(query: String): List {
- val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
+ val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val returnTitles: MutableList = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt
index ca47b50ab6e..534c3ac5e12 100644
--- a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt
@@ -3,16 +3,13 @@ package ani.dantotsu.parsers
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
-import ani.dantotsu.logger
import ani.dantotsu.media.manga.MangaNameAdapter
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
-class OfflineNovelParser: NovelParser() {
+class OfflineNovelParser : NovelParser() {
private val downloadManager = Injekt.get()
override val hostUrl: String = "Offline"
@@ -34,7 +31,7 @@ class OfflineNovelParser: NovelParser() {
if (it.isDirectory) {
val chapter = Book(
it.name,
- it.absolutePath + "/cover.jpg",
+ it.absolutePath + "/cover.jpg",
null,
listOf(it.absolutePath + "/0.epub")
)
@@ -53,7 +50,7 @@ class OfflineNovelParser: NovelParser() {
}
override suspend fun search(query: String): List {
- val titles = downloadManager.novelDownloads.map { it.title }.distinct()
+ val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val returnTitles: MutableList = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
@@ -75,7 +72,8 @@ class OfflineNovelParser: NovelParser() {
}
}
}
- val cover = currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
+ val cover =
+ currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
names.forEach {
returnList.add(ShowResponse(it, it, cover))
}
diff --git a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt
index ce56182c70c..582a7cb4bff 100644
--- a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt
@@ -57,10 +57,13 @@ data class VideoServer(
val name: String,
val embed: FileUrl,
val extraData: Map? = null,
- val video: eu.kanade.tachiyomi.animesource.model.Video? = null
+ val video: eu.kanade.tachiyomi.animesource.model.Video? = null,
+ val offline: Boolean = false
) : Serializable {
constructor(name: String, embedUrl: String, extraData: Map? = null)
: this(name, FileUrl(embedUrl), extraData)
+ constructor(name: String, offline: Boolean, extraData: Map?)
+ : this(name, FileUrl(""), extraData, null, offline)
constructor(
name: String,
diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt
index 13da2ce2620..e23ec62fb21 100644
--- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt
@@ -22,10 +22,10 @@ class DynamicNovelParser(extension: NovelExtension.Installed) : NovelParser() {
override suspend fun search(query: String): List {
val source = extension.sources.firstOrNull()
- if (source is NovelInterface) {
- return source.search(query, client)
+ return if (source is NovelInterface) {
+ source.search(query, client)
} else {
- return emptyList()
+ emptyList()
}
}
diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt
index b0f6693b29f..3c26530cb59 100644
--- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt
@@ -111,11 +111,12 @@ internal object NovelExtensionLoader {
@Suppress("DEPRECATION")
private fun getSignatureHash(pkgInfo: PackageInfo): List? {
- val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) {
- pkgInfo.signingInfo.apkContentsSigners
- } else {
- pkgInfo.signatures
- }
+ val signatures =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) {
+ pkgInfo.signingInfo.apkContentsSigners
+ } else {
+ pkgInfo.signatures
+ }
return if (!signatures.isNullOrEmpty()) {
signatures.map { Hash.sha256(it.toByteArray()) }
} else {
diff --git a/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt
index bbe3fcd32f7..b1cd429bda5 100644
--- a/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt
+++ b/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt
@@ -11,6 +11,7 @@ data class CurrentNovelReaderSettings(
var justify: Boolean = true,
var hyphenation: Boolean = true,
var useDarkTheme: Boolean = false,
+ var useOledTheme: Boolean = false,
var invert: Boolean = false,
var maxInlineSize: Int = 720,
var maxBlockSize: Int = 1440,
diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
index 32e3a7e7f53..04701603c98 100644
--- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
@@ -6,11 +6,10 @@ import android.os.Build.VERSION.*
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
+import android.view.View
import android.view.ViewGroup
import android.widget.AutoCompleteTextView
-import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.PopupMenu
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
@@ -23,12 +22,8 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
class ExtensionsActivity : AppCompatActivity() {
- private val restartMainActivity = object : OnBackPressedCallback(false) {
- override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity)
- }
lateinit var binding: ActivityExtensionsBinding
-
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -119,17 +114,18 @@ class ExtensionsActivity : AppCompatActivity() {
initActivity(this)
-/* TODO
- binding.languageselect.setOnClickListener {
- val popup = PopupMenu(this, it)
- popup.inflate(R.menu.launguage_selector_menu)
- popup.setOnMenuItemClickListener { menuItem ->
- true
- }
- popup.setOnDismissListener {
- }
- popup.show()
- }*/
+ binding.languageselect.visibility = View.GONE
+ /* TODO
+ binding.languageselect.setOnClickListener {
+ val popup = PopupMenu(this, it)
+ popup.inflate(R.menu.launguage_selector_menu)
+ popup.setOnMenuItemClickListener { menuItem ->
+ true
+ }
+ popup.setOnDismissListener {
+ }
+ popup.show()
+ }*/
binding.settingsContainer.updateLayoutParams {
topMargin = statusBarHeight
bottomMargin = navBarHeight
diff --git a/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt b/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt
index c03ba9e1517..74d9525d8f2 100644
--- a/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt
@@ -18,88 +18,83 @@ class FAQActivity : AppCompatActivity() {
Triple(
R.drawable.ic_round_help_24,
- currContext()!!.getString(R.string.question_1),
- currContext()!!.getString(R.string.answer_1)
+ currContext()?.getString(R.string.question_1) ?: "",
+ currContext()?.getString(R.string.answer_1) ?: ""
),
Triple(
R.drawable.ic_round_auto_awesome_24,
- currContext()!!.getString(R.string.question_2),
- currContext()!!.getString(R.string.answer_2)
+ currContext()?.getString(R.string.question_2) ?: "",
+ currContext()?.getString(R.string.answer_2) ?: ""
),
Triple(
R.drawable.ic_round_auto_awesome_24,
- currContext()!!.getString(R.string.question_17),
- currContext()!!.getString(R.string.answer_17)
+ currContext()?.getString(R.string.question_17) ?: "",
+ currContext()?.getString(R.string.answer_17) ?: ""
),
Triple(
R.drawable.ic_round_download_24,
- currContext()!!.getString(R.string.question_3),
- currContext()!!.getString(R.string.answer_3)
+ currContext()?.getString(R.string.question_3) ?: "",
+ currContext()?.getString(R.string.answer_3) ?: ""
),
Triple(
R.drawable.ic_round_help_24,
- currContext()!!.getString(R.string.question_16),
- currContext()!!.getString(R.string.answer_16)
+ currContext()?.getString(R.string.question_16) ?: "",
+ currContext()?.getString(R.string.answer_16) ?: ""
),
Triple(
R.drawable.ic_round_dns_24,
- currContext()!!.getString(R.string.question_4),
- currContext()!!.getString(R.string.answer_4)
+ currContext()?.getString(R.string.question_4) ?: "",
+ currContext()?.getString(R.string.answer_4) ?: ""
),
Triple(
R.drawable.ic_baseline_screen_lock_portrait_24,
- currContext()!!.getString(R.string.question_5),
- currContext()!!.getString(R.string.answer_5)
+ currContext()?.getString(R.string.question_5) ?: "",
+ currContext()?.getString(R.string.answer_5) ?: ""
),
Triple(
R.drawable.ic_anilist,
- currContext()!!.getString(R.string.question_6),
- currContext()!!.getString(R.string.answer_6)
+ currContext()?.getString(R.string.question_6) ?: "",
+ currContext()?.getString(R.string.answer_6) ?: ""
),
Triple(
R.drawable.ic_round_movie_filter_24,
- currContext()!!.getString(R.string.question_7),
- currContext()!!.getString(R.string.answer_7)
- ),
- Triple(
- R.drawable.ic_round_menu_book_24,
- currContext()!!.getString(R.string.question_8),
- currContext()!!.getString(R.string.answer_8)
+ currContext()?.getString(R.string.question_7) ?: "",
+ currContext()?.getString(R.string.answer_7) ?: ""
),
Triple(
R.drawable.ic_round_lock_open_24,
- currContext()!!.getString(R.string.question_9),
- currContext()!!.getString(R.string.answer_9)
+ currContext()?.getString(R.string.question_9) ?: "",
+ currContext()?.getString(R.string.answer_9) ?: ""
),
Triple(
R.drawable.ic_round_smart_button_24,
- currContext()!!.getString(R.string.question_10),
- currContext()!!.getString(R.string.answer_10)
+ currContext()?.getString(R.string.question_10) ?: "",
+ currContext()?.getString(R.string.answer_10) ?: ""
),
Triple(
R.drawable.ic_round_smart_button_24,
- currContext()!!.getString(R.string.question_11),
- currContext()!!.getString(R.string.answer_11)
+ currContext()?.getString(R.string.question_11) ?: "",
+ currContext()?.getString(R.string.answer_11) ?: ""
),
Triple(
R.drawable.ic_round_info_24,
- currContext()!!.getString(R.string.question_12),
- currContext()!!.getString(R.string.answer_12)
+ currContext()?.getString(R.string.question_12) ?: "",
+ currContext()?.getString(R.string.answer_12) ?: ""
),
Triple(
R.drawable.ic_round_help_24,
- currContext()!!.getString(R.string.question_13),
- currContext()!!.getString(R.string.answer_13)
+ currContext()?.getString(R.string.question_13) ?: "",
+ currContext()?.getString(R.string.answer_13) ?: ""
),
Triple(
R.drawable.ic_round_art_track_24,
- currContext()!!.getString(R.string.question_14),
- currContext()!!.getString(R.string.answer_14)
+ currContext()?.getString(R.string.question_14) ?: "",
+ currContext()?.getString(R.string.answer_14) ?: ""
),
Triple(
R.drawable.ic_round_video_settings_24,
- currContext()!!.getString(R.string.question_15),
- currContext()!!.getString(R.string.answer_15)
+ currContext()?.getString(R.string.question_15) ?: "",
+ currContext()?.getString(R.string.answer_15) ?: ""
)
)
}
diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt
index bdbcb7caf3d..6f83009a7fb 100644
--- a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt
+++ b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt
@@ -1,5 +1,6 @@
package ani.dantotsu.settings
+import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.Context
@@ -45,90 +46,84 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentAnimeExtensionsBinding? = null
private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView
- val skipIcons = loadData("skip_extension_icons") ?: false
+ private val skipIcons = loadData("skip_extension_icons") ?: false
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
private val extensionsAdapter = AnimeExtensionsAdapter(
{ pkg ->
+ val name = pkg.name
+ val changeUIVisibility: (Boolean) -> Unit = { show ->
+ val activity = requireActivity() as ExtensionsActivity
+ val visibility = if (show) View.VISIBLE else View.GONE
+ activity.findViewById(R.id.viewPager).visibility = visibility
+ activity.findViewById(R.id.tabLayout).visibility = visibility
+ activity.findViewById(R.id.searchView).visibility = visibility
+ activity.findViewById(R.id.languageselect).visibility = visibility
+ activity.findViewById(R.id.extensions).text =
+ if (show) getString(R.string.extensions) else name
+ activity.findViewById(R.id.fragmentExtensionsContainer).visibility =
+ if (show) View.GONE else View.VISIBLE
+ }
+ var itemSelected = false
val allSettings = pkg.sources.filterIsInstance()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
- val names = allSettings.map { it.lang }.toTypedArray()
+ val names = allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
+ .toTypedArray()
var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
+ itemSelected = true
selectedIndex = which
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
- // Move the fragment transaction here
- val eActivity = requireActivity() as ExtensionsActivity
- eActivity.runOnUiThread {
- val fragment =
- AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
-
- eActivity.findViewById(R.id.viewPager).visibility =
- View.VISIBLE
- eActivity.findViewById(R.id.tabLayout).visibility =
- View.VISIBLE
- eActivity.findViewById(R.id.searchView).visibility =
- View.VISIBLE
- eActivity.findViewById(R.id.fragmentExtensionsContainer).visibility =
- View.GONE
- }
- parentFragmentManager.beginTransaction()
- .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
- .replace(R.id.fragmentExtensionsContainer, fragment)
- .addToBackStack(null)
- .commit()
+ val fragment =
+ AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
+ changeUIVisibility(true)
+ }
+ parentFragmentManager.beginTransaction()
+ .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
+ .replace(R.id.fragmentExtensionsContainer, fragment)
+ .addToBackStack(null)
+ .commit()
+ }
+ .setOnDismissListener {
+ if (!itemSelected) {
+ changeUIVisibility(true)
}
}
.show()
dialog.window?.setDimAmount(0.8f)
} else {
// If there's only one setting, proceed with the fragment transaction
- val eActivity = requireActivity() as ExtensionsActivity
- eActivity.runOnUiThread {
- val fragment =
- AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
+ val fragment =
+ AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
+ changeUIVisibility(true)
+ }
+ parentFragmentManager.beginTransaction()
+ .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
+ .replace(R.id.fragmentExtensionsContainer, fragment)
+ .addToBackStack(null)
+ .commit()
- eActivity.findViewById(R.id.viewPager).visibility =
- View.VISIBLE
- eActivity.findViewById(R.id.tabLayout).visibility =
- View.VISIBLE
- eActivity.findViewById(R.id.searchView).visibility =
- View.VISIBLE
- eActivity.findViewById(R.id.fragmentExtensionsContainer).visibility =
- View.GONE
- }
- parentFragmentManager.beginTransaction()
- .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
- .replace(R.id.fragmentExtensionsContainer, fragment)
- .addToBackStack(null)
- .commit()
- }
}
// Hide ViewPager2 and TabLayout
- val activity = requireActivity() as ExtensionsActivity
- activity.findViewById(R.id.viewPager).visibility = View.GONE
- activity.findViewById(R.id.tabLayout).visibility = View.GONE
- activity.findViewById(R.id.searchView).visibility = View.GONE
- activity.findViewById(R.id.fragmentExtensionsContainer).visibility =
- View.VISIBLE
+ changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
},
- { pkg ->
+ { pkg, forceDelete ->
if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
- if (pkg.hasUpdate) {
+ if (pkg.hasUpdate && !forceDelete) {
animeExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
@@ -209,7 +204,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private class AnimeExtensionsAdapter(
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
- private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
+ private val onUninstallClicked: (AnimeExtension.Installed, Boolean) -> Unit,
val skipIcons: Boolean
) : ListAdapter(
DIFF_CALLBACK_INSTALLED
@@ -225,6 +220,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
return ViewHolder(view)
}
+ @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter
val nsfw = if (extension.isNsfw) "(18+)" else ""
@@ -240,11 +236,15 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
}
holder.closeTextView.setOnClickListener {
- onUninstallClicked(extension)
+ onUninstallClicked(extension, false)
}
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
+ holder.card.setOnLongClickListener {
+ onUninstallClicked(extension, true)
+ true
+ }
}
fun filter(query: String, currentList: List) {
@@ -264,6 +264,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
+ val card = view.findViewById(R.id.extensionCardView)
}
companion object {
diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt
index ecef2e58eb7..1ad5228a547 100644
--- a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt
+++ b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt
@@ -1,6 +1,7 @@
package ani.dantotsu.settings
+import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.Context
@@ -44,70 +45,84 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentMangaExtensionsBinding? = null
private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView
- val skipIcons = loadData("skip_extension_icons") ?: false
+ private val skipIcons = loadData("skip_extension_icons") ?: false
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
- private val extensionsAdapter = MangaExtensionsAdapter({ pkg ->
- val changeUIVisibility: (Boolean) -> Unit = { show ->
- val activity = requireActivity() as ExtensionsActivity
- val visibility = if (show) View.VISIBLE else View.GONE
- activity.findViewById(R.id.viewPager).visibility = visibility
- activity.findViewById(R.id.tabLayout).visibility = visibility
- activity.findViewById(R.id.searchView).visibility = visibility
- activity.findViewById(R.id.fragmentExtensionsContainer).visibility =
- if (show) View.GONE else View.VISIBLE
- }
- val allSettings = pkg.sources.filterIsInstance()
- if (allSettings.isNotEmpty()) {
- var selectedSetting = allSettings[0]
- if (allSettings.size > 1) {
- val names = allSettings.map { it.lang }.toTypedArray()
- var selectedIndex = 0
- val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
- .setTitle("Select a Source")
- .setSingleChoiceItems(names, selectedIndex) { dialog, which ->
- selectedIndex = which
- selectedSetting = allSettings[selectedIndex]
- dialog.dismiss()
+ private val extensionsAdapter = MangaExtensionsAdapter(
+ { pkg ->
+ val name = pkg.name
+ val changeUIVisibility: (Boolean) -> Unit = { show ->
+ val activity = requireActivity() as ExtensionsActivity
+ val visibility = if (show) View.VISIBLE else View.GONE
+ activity.findViewById(R.id.viewPager).visibility = visibility
+ activity.findViewById(R.id.tabLayout).visibility = visibility
+ activity.findViewById(R.id.searchView).visibility = visibility
+ activity.findViewById(R.id.languageselect).visibility = visibility
+ activity.findViewById(R.id.extensions).text =
+ if (show) getString(R.string.extensions) else name
+ activity.findViewById(R.id.fragmentExtensionsContainer).visibility =
+ if (show) View.GONE else View.VISIBLE
+ }
+ var itemSelected = false
+ val allSettings = pkg.sources.filterIsInstance()
+ if (allSettings.isNotEmpty()) {
+ var selectedSetting = allSettings[0]
+ if (allSettings.size > 1) {
+ val names = allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
+ .toTypedArray()
+ var selectedIndex = 0
+ val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
+ .setTitle("Select a Source")
+ .setSingleChoiceItems(names, selectedIndex) { dialog, which ->
+ itemSelected = true
+ selectedIndex = which
+ selectedSetting = allSettings[selectedIndex]
+ dialog.dismiss()
- // Move the fragment transaction here
- val fragment =
- MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
+ // Move the fragment transaction here
+ val fragment =
+ MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
+ changeUIVisibility(true)
+ }
+ parentFragmentManager.beginTransaction()
+ .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
+ .replace(R.id.fragmentExtensionsContainer, fragment)
+ .addToBackStack(null)
+ .commit()
+ }
+ .setOnDismissListener {
+ if (!itemSelected) {
changeUIVisibility(true)
}
- parentFragmentManager.beginTransaction()
- .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
- .replace(R.id.fragmentExtensionsContainer, fragment)
- .addToBackStack(null)
- .commit()
- }
- .show()
- dialog.window?.setDimAmount(0.8f)
- } else {
- // If there's only one setting, proceed with the fragment transaction
- val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
- changeUIVisibility(true)
+ }
+ .show()
+ dialog.window?.setDimAmount(0.8f)
+ } else {
+ // If there's only one setting, proceed with the fragment transaction
+ val fragment =
+ MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
+ changeUIVisibility(true)
+ }
+ parentFragmentManager.beginTransaction()
+ .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
+ .replace(R.id.fragmentExtensionsContainer, fragment)
+ .addToBackStack(null)
+ .commit()
}
- parentFragmentManager.beginTransaction()
- .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
- .replace(R.id.fragmentExtensionsContainer, fragment)
- .addToBackStack(null)
- .commit()
- }
- // Hide ViewPager2 and TabLayout
- changeUIVisibility(false)
- } else {
- Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
- .show()
- }
- },
- { pkg ->
+ // Hide ViewPager2 and TabLayout
+ changeUIVisibility(false)
+ } else {
+ Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
+ .show()
+ }
+ },
+ { pkg: MangaExtension.Installed, forceDelete: Boolean ->
if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
- if (pkg.hasUpdate) {
+ if (pkg.hasUpdate && !forceDelete) {
mangaExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
@@ -188,7 +203,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
private class MangaExtensionsAdapter(
private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
- private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
+ private val onUninstallClicked: (MangaExtension.Installed, Boolean) -> Unit,
skipIcons: Boolean
) : ListAdapter(
DIFF_CALLBACK_INSTALLED
@@ -206,6 +221,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
return ViewHolder(view)
}
+ @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter
val nsfw = if (extension.isNsfw) "(18+)" else ""
@@ -221,11 +237,16 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
}
holder.closeTextView.setOnClickListener {
- onUninstallClicked(extension)
+ onUninstallClicked(extension, false)
}
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
+
+ holder.card.setOnLongClickListener {
+ onUninstallClicked(extension, true)
+ true
+ }
}
fun filter(query: String, currentList: List) {
@@ -245,6 +266,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
+ val card: View = view.findViewById(R.id.extensionCardView)
}
companion object {
diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt
index c0a61dd5aa8..76b84bca55a 100644
--- a/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt
+++ b/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt
@@ -39,17 +39,18 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
private lateinit var extensionsRecyclerView: RecyclerView
val skipIcons = loadData("skip_extension_icons") ?: false
private val novelExtensionManager: NovelExtensionManager = Injekt.get()
- private val extensionsAdapter = NovelExtensionsAdapter({ pkg ->
- Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
- .show()
- },
+ private val extensionsAdapter = NovelExtensionsAdapter(
{ pkg ->
+ Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
+ .show()
+ },
+ { pkg, forceDelete ->
if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
- if (pkg.hasUpdate) {
+ if (pkg.hasUpdate && !forceDelete) {
novelExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
@@ -130,7 +131,7 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
private class NovelExtensionsAdapter(
private val onSettingsClicked: (NovelExtension.Installed) -> Unit,
- private val onUninstallClicked: (NovelExtension.Installed) -> Unit,
+ private val onUninstallClicked: (NovelExtension.Installed, Boolean) -> Unit,
skipIcons: Boolean
) : ListAdapter(
DIFF_CALLBACK_INSTALLED
@@ -165,11 +166,15 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
}
holder.closeTextView.setOnClickListener {
- onUninstallClicked(extension)
+ onUninstallClicked(extension, false)
}
holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension)
}
+ holder.card.setOnLongClickListener {
+ onUninstallClicked(extension, true)
+ true
+ }
}
fun filter(query: String, currentList: List) {
@@ -189,6 +194,7 @@ class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler {
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
+ val card = view.findViewById(R.id.extensionCardView)
}
companion object {
diff --git a/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt
deleted file mode 100644
index 7c72c7e9bdb..00000000000
--- a/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package ani.dantotsu.settings
-
-import java.io.Serializable
-
-data class NovelReaderSettings(
- var showSource: Boolean = true,
- var showSystemBars: Boolean = false,
- var default: CurrentNovelReaderSettings = CurrentNovelReaderSettings(),
- var askIndividual: Boolean = true,
-) : Serializable
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt
index 1589d66da63..83037c4c420 100644
--- a/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt
+++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt
@@ -22,7 +22,7 @@ data class PlayerSettings(
//TimeStamps
var timeStampsEnabled: Boolean = true,
- var useProxyForTimeStamps: Boolean = true,
+ var useProxyForTimeStamps: Boolean = false,
var showTimeStampButton: Boolean = true,
//Auto
@@ -45,6 +45,6 @@ data class PlayerSettings(
var skipTime: Int = 85,
//Other
- var cast: Boolean = false,
+ var cast: Boolean = true,
var pip: Boolean = true
) : Serializable
diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt
index ff3d704dc8d..88167079093 100644
--- a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt
@@ -97,7 +97,21 @@ class PlayerSettingsActivity : AppCompatActivity() {
val speeds =
- arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f)
+ arrayOf(
+ 0.25f,
+ 0.33f,
+ 0.5f,
+ 0.66f,
+ 0.75f,
+ 1f,
+ 1.15f,
+ 1.25f,
+ 1.33f,
+ 1.5f,
+ 1.66f,
+ 1.75f,
+ 2f
+ )
val cursedSpeeds = arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
var curSpeedArr = if (settings.cursedSpeeds) cursedSpeeds else speeds
var speedsName = curSpeedArr.map { "${it}x" }.toTypedArray()
@@ -106,13 +120,14 @@ class PlayerSettingsActivity : AppCompatActivity() {
val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.default_speed))
binding.playerSettingsSpeed.setOnClickListener {
- val dialog = speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i ->
- settings.defaultSpeed = i
- binding.playerSettingsSpeed.text =
- getString(R.string.default_playback_speed, speedsName[i])
- saveData(player, settings)
- dialog.dismiss()
- }.show()
+ val dialog =
+ speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i ->
+ settings.defaultSpeed = i
+ binding.playerSettingsSpeed.text =
+ getString(R.string.default_playback_speed, speedsName[i])
+ saveData(player, settings)
+ dialog.dismiss()
+ }.show()
dialog.window?.setDimAmount(0.8f)
}
@@ -256,11 +271,12 @@ class PlayerSettingsActivity : AppCompatActivity() {
val resizeDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.default_resize_mode))
binding.playerResizeMode.setOnClickListener {
- val dialog = resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count ->
- settings.resize = count
- saveData(player, settings)
- dialog.dismiss()
- }.show()
+ val dialog =
+ resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count ->
+ settings.resize = count
+ saveData(player, settings)
+ dialog.dismiss()
+ }.show()
dialog.window?.setDimAmount(0.8f)
}
fun restartApp() {
@@ -382,7 +398,10 @@ class PlayerSettingsActivity : AppCompatActivity() {
val outlineDialog = AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.outline_type))
binding.videoSubOutline.setOnClickListener {
- val dialog = outlineDialog.setSingleChoiceItems(typesOutline, settings.outline) { dialog, count ->
+ val dialog = outlineDialog.setSingleChoiceItems(
+ typesOutline,
+ settings.outline
+ ) { dialog, count ->
settings.outline = count
saveData(player, settings)
dialog.dismiss()
diff --git a/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt
index 8a249dd7538..e0a91af8bb0 100644
--- a/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt
+++ b/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt
@@ -8,6 +8,7 @@ data class ReaderSettings(
var autoDetectWebtoon: Boolean = true,
var default: CurrentReaderSettings = CurrentReaderSettings(),
+ var defaultLN: CurrentNovelReaderSettings = CurrentNovelReaderSettings(),
var askIndividual: Boolean = true,
var updateForH: Boolean = false
diff --git a/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt
index 61c87afe596..33a6ddf20e9 100644
--- a/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt
@@ -42,7 +42,7 @@ class ReaderSettingsActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed()
}
- //General
+ //Manga Settings
binding.readerSettingsSourceName.isChecked = settings.showSource
binding.readerSettingsSourceName.setOnCheckedChangeListener { _, isChecked ->
settings.showSource = isChecked
@@ -54,14 +54,14 @@ class ReaderSettingsActivity : AppCompatActivity() {
settings.showSystemBars = isChecked
saveData(reader, settings)
}
-
+ //Default Manga
binding.readerSettingsAutoWebToon.isChecked = settings.autoDetectWebtoon
binding.readerSettingsAutoWebToon.setOnCheckedChangeListener { _, isChecked ->
settings.autoDetectWebtoon = isChecked
saveData(reader, settings)
}
- //Default
+
val layoutList = listOf(
binding.readerSettingsPaged,
binding.readerSettingsContinuousPaged,
@@ -185,6 +185,169 @@ class ReaderSettingsActivity : AppCompatActivity() {
saveData(reader, settings)
}
+ //LN settings
+ val layoutListLN = listOf(
+ binding.LNpaged,
+ binding.LNcontinuous
+ )
+
+ binding.LNlayoutText.text = settings.defaultLN.layout.string
+ var selectedLN = layoutListLN[settings.defaultLN.layout.ordinal]
+ selectedLN.alpha = 1f
+
+ layoutListLN.forEachIndexed { index, imageButton ->
+ imageButton.setOnClickListener {
+ selectedLN.alpha = 0.33f
+ selectedLN = imageButton
+ selectedLN.alpha = 1f
+ settings.defaultLN.layout = CurrentNovelReaderSettings.Layouts[index]
+ ?: CurrentNovelReaderSettings.Layouts.PAGED
+ binding.LNlayoutText.text = settings.defaultLN.layout.string
+ saveData(reader, settings)
+ }
+ }
+
+ val dualListLN = listOf(
+ binding.LNdualNo,
+ binding.LNdualAuto,
+ binding.LNdualForce
+ )
+
+ binding.LNdualPageText.text = settings.defaultLN.dualPageMode.toString()
+ var selectedDualLN = dualListLN[settings.defaultLN.dualPageMode.ordinal]
+ selectedDualLN.alpha = 1f
+
+ dualListLN.forEachIndexed { index, imageButton ->
+ imageButton.setOnClickListener {
+ selectedDualLN.alpha = 0.33f
+ selectedDualLN = imageButton
+ selectedDualLN.alpha = 1f
+ settings.defaultLN.dualPageMode = CurrentReaderSettings.DualPageModes[index]
+ ?: CurrentReaderSettings.DualPageModes.Automatic
+ binding.LNdualPageText.text = settings.defaultLN.dualPageMode.toString()
+ saveData(reader, settings)
+ }
+ }
+
+ binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
+ binding.LNlineHeight.setOnFocusChangeListener { _, hasFocus ->
+ if (!hasFocus) {
+ val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
+ settings.defaultLN.lineHeight = value
+ binding.LNlineHeight.setText(value.toString())
+ saveData(reader, settings)
+ }
+ }
+
+ binding.LNincrementLineHeight.setOnClickListener {
+ val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
+ settings.defaultLN.lineHeight = value + 0.1f
+ binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
+ saveData(reader, settings)
+ }
+
+ binding.LNdecrementLineHeight.setOnClickListener {
+ val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f
+ settings.defaultLN.lineHeight = value - 0.1f
+ binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString())
+ saveData(reader, settings)
+ }
+
+ binding.LNmargin.setText(settings.defaultLN.margin.toString())
+ binding.LNmargin.setOnFocusChangeListener { _, hasFocus ->
+ if (!hasFocus) {
+ val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
+ settings.defaultLN.margin = value
+ binding.LNmargin.setText(value.toString())
+ saveData(reader, settings)
+ }
+ }
+
+ binding.LNincrementMargin.setOnClickListener {
+ val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
+ settings.defaultLN.margin = value + 0.01f
+ binding.LNmargin.setText(settings.defaultLN.margin.toString())
+ saveData(reader, settings)
+ }
+
+ binding.LNdecrementMargin.setOnClickListener {
+ val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f
+ settings.defaultLN.margin = value - 0.01f
+ binding.LNmargin.setText(settings.defaultLN.margin.toString())
+ saveData(reader, settings)
+ }
+
+ binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
+ binding.LNmaxInlineSize.setOnFocusChangeListener { _, hasFocus ->
+ if (!hasFocus) {
+ val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
+ settings.defaultLN.maxInlineSize = value
+ binding.LNmaxInlineSize.setText(value.toString())
+ saveData(reader, settings)
+ }
+ }
+
+ binding.LNincrementMaxInlineSize.setOnClickListener {
+ val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
+ settings.defaultLN.maxInlineSize = value + 10
+ binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
+ saveData(reader, settings)
+ }
+
+ binding.LNdecrementMaxInlineSize.setOnClickListener {
+ val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720
+ settings.defaultLN.maxInlineSize = value - 10
+ binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString())
+ saveData(reader, settings)
+ }
+
+ binding.LNmaxBlockSize.setText(settings.defaultLN.maxBlockSize.toString())
+ binding.LNmaxBlockSize.setOnFocusChangeListener { _, hasFocus ->
+ if (!hasFocus) {
+ val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
+ settings.defaultLN.maxBlockSize = value
+ binding.LNmaxBlockSize.setText(value.toString())
+ saveData(reader, settings)
+ }
+ }
+ binding.LNincrementMaxBlockSize.setOnClickListener {
+ val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
+ settings.defaultLN.maxInlineSize = value + 10
+ binding.LNmaxBlockSize.setText(settings.defaultLN.maxInlineSize.toString())
+ saveData(reader, settings)
+ }
+
+ binding.LNdecrementMaxBlockSize.setOnClickListener {
+ val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720
+ settings.defaultLN.maxBlockSize = value - 10
+ binding.LNmaxBlockSize.setText(settings.defaultLN.maxBlockSize.toString())
+ saveData(reader, settings)
+ }
+
+ binding.LNuseDarkTheme.isChecked = settings.defaultLN.useDarkTheme
+ binding.LNuseDarkTheme.setOnCheckedChangeListener { _, isChecked ->
+ settings.defaultLN.useDarkTheme = isChecked
+ saveData(reader, settings)
+ }
+
+ binding.LNuseOledTheme.isChecked = settings.defaultLN.useOledTheme
+ binding.LNuseOledTheme.setOnCheckedChangeListener { _, isChecked ->
+ settings.defaultLN.useOledTheme = isChecked
+ saveData(reader, settings)
+ }
+
+ binding.LNkeepScreenOn.isChecked = settings.defaultLN.keepScreenOn
+ binding.LNkeepScreenOn.setOnCheckedChangeListener { _, isChecked ->
+ settings.defaultLN.keepScreenOn = isChecked
+ saveData(reader, settings)
+ }
+
+ binding.LNvolumeButton.isChecked = settings.defaultLN.volumeButtons
+ binding.LNvolumeButton.setOnCheckedChangeListener { _, isChecked ->
+ settings.defaultLN.volumeButtons = isChecked
+ saveData(reader, settings)
+ }
+
//Update Progress
binding.readerSettingsAskUpdateProgress.isChecked = settings.askIndividual
binding.readerSettingsAskUpdateProgress.setOnCheckedChangeListener { _, isChecked ->
diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt
index 6d1e70df0bb..d140224b8e7 100644
--- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
+import android.graphics.Color
import android.graphics.drawable.Animatable
import android.os.Build.*
import android.os.Build.VERSION.*
@@ -11,19 +12,24 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
-import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
+import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.ActivitySettingsBinding
+import ani.dantotsu.download.DownloadedType
+import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.others.AppUpdater
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.LangSet
@@ -37,7 +43,9 @@ import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
-import com.skydoves.colorpickerview.listeners.ColorListener
+import eltos.simpledialogfragment.SimpleDialog
+import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE
+import eltos.simpledialogfragment.color.SimpleColorDialog
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.network.NetworkPreferences
import io.noties.markwon.Markwon
@@ -50,7 +58,7 @@ import uy.kohesive.injekt.api.get
import kotlin.random.Random
-class SettingsActivity : AppCompatActivity() {
+class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListener {
private val restartMainActivity = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
}
@@ -59,6 +67,7 @@ class SettingsActivity : AppCompatActivity() {
private val networkPreferences = Injekt.get()
private var cursedCounter = 0
+ @OptIn(UnstableApi::class)
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -71,26 +80,7 @@ class SettingsActivity : AppCompatActivity() {
binding.settingsVersion.text = getString(R.string.version_current, BuildConfig.VERSION_NAME)
binding.settingsVersion.setOnLongClickListener {
- fun getArch(): String {
- SUPPORTED_ABIS.forEach {
- when (it) {
- "arm64-v8a" -> return "aarch64"
- "armeabi-v7a" -> return "arm"
- "x86_64" -> return "x86_64"
- "x86" -> return "i686"
- }
- }
- return System.getProperty("os.arch") ?: System.getProperty("os.product.cpu.abi")
- ?: "Unknown Architecture"
- }
-
- val info = """
- dantotsu Version: ${BuildConfig.VERSION_NAME}
- Device: $BRAND $DEVICE
- Architecture: ${getArch()}
- OS Version: $CODENAME $RELEASE ($SDK_INT)
- """.trimIndent()
- copyToClipboard(info, false)
+ copyToClipboard(getDeviceInfo(), false)
toast(getString(R.string.copied_device_info))
return@setOnLongClickListener true
}
@@ -176,34 +166,30 @@ class SettingsActivity : AppCompatActivity() {
binding.customTheme.setOnClickListener {
- var passedColor: Int = 0
- val dialogView = layoutInflater.inflate(R.layout.dialog_color_picker, null)
- val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
- .setTitle("Custom Theme")
- .setView(dialogView)
- .setPositiveButton("OK") { dialog, _ ->
- getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
- .putInt("custom_theme_int", passedColor).apply()
- logger("Custom Theme: $passedColor")
- dialog.dismiss()
+ val originalColor = getSharedPreferences("Dantotsu", MODE_PRIVATE).getInt(
+ "custom_theme_int",
+ Color.parseColor("#6200EE")
+ )
+
+ class CustomColorDialog : SimpleColorDialog() { //idk where to put it
+ override fun onPositiveButtonClick() {
restartApp()
+ super.onPositiveButtonClick()
}
- .setNegativeButton("Cancel") { dialog, _ ->
- dialog.dismiss()
- }
- .create()
- val colorPickerView =
- dialogView.findViewById(R.id.colorPickerView)
- colorPickerView.setColorListener(ColorListener { color, fromUser ->
- val linearLayout = dialogView.findViewById(R.id.linear)
- passedColor = color
- linearLayout.setBackgroundColor(color)
- })
- alertDialog.show()
- alertDialog.window?.setDimAmount(0.8f)
+ }
+
+ val tag = "colorPicker"
+ CustomColorDialog().title("Custom Theme")
+ .colorPreset(originalColor)
+ .colors(this, SimpleColorDialog.BEIGE_COLOR_PALLET)
+ .allowCustom(true)
+ .showOutline(0x46000000)
+ .gridNumColumn(5)
+ .choiceMode(SimpleColorDialog.SINGLE_CHOICE)
+ .neg()
+ .show(this, tag)
}
- //val animeSource = loadData("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0
val animeSource = getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
@@ -228,6 +214,47 @@ class SettingsActivity : AppCompatActivity() {
binding.animeSource.clearFocus()
}
+ binding.settingsPinnedAnimeSources.setOnClickListener {
+ val animeSourcesWithoutDownloadsSource = AnimeSources.list.filter { it.name != "Downloaded" }
+ val names = animeSourcesWithoutDownloadsSource.map { it.name }
+ val pinnedSourcesBoolean = animeSourcesWithoutDownloadsSource.map { it.name in AnimeSources.pinnedAnimeSources }
+ val pinnedSourcesOriginal = getSharedPreferences(
+ "Dantotsu",
+ Context.MODE_PRIVATE
+ ).getStringSet("pinned_anime_sources", null)
+ val pinnedSources = pinnedSourcesOriginal?.toMutableSet() ?: mutableSetOf()
+ val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
+ .setTitle("Pinned Anime Sources")
+ .setMultiChoiceItems(
+ names.toTypedArray(),
+ pinnedSourcesBoolean.toBooleanArray()
+ ) { _, which, isChecked ->
+ if (isChecked) {
+ pinnedSources.add(AnimeSources.names[which])
+ } else {
+ pinnedSources.remove(AnimeSources.names[which])
+ }
+ }
+ .setPositiveButton("OK") { dialog, _ ->
+ val oldDefaultSourceIndex = getSharedPreferences(
+ "Dantotsu",
+ Context.MODE_PRIVATE
+ ).getInt("settings_def_anime_source_s_r", 0)
+ val oldName = AnimeSources.names[oldDefaultSourceIndex]
+ getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
+ .putStringSet("pinned_anime_sources", pinnedSources).apply()
+ AnimeSources.pinnedAnimeSources = pinnedSources
+ AnimeSources.performReorderAnimeSources()
+ val newDefaultSourceIndex = AnimeSources.names.indexOf(oldName)
+ getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
+ .putInt("settings_def_anime_source_s_r", newDefaultSourceIndex).apply()
+ dialog.dismiss()
+ }
+ .create()
+ alertDialog.show()
+ alertDialog.window?.setDimAmount(0.8f)
+ }
+
binding.settingsPlayer.setOnClickListener {
startActivity(Intent(this, PlayerSettingsActivity::class.java))
}
@@ -237,7 +264,10 @@ class SettingsActivity : AppCompatActivity() {
AlertDialog.Builder(this, R.style.DialogTheme).setTitle("Download Manager")
var downloadManager = loadData("settings_download_manager") ?: 0
binding.settingsDownloadManager.setOnClickListener {
- val dialog = downloadManagerDialog.setSingleChoiceItems(managers, downloadManager) { dialog, count ->
+ val dialog = downloadManagerDialog.setSingleChoiceItems(
+ managers,
+ downloadManager
+ ) { dialog, count ->
downloadManager = count
saveData("settings_download_manager", downloadManager)
dialog.dismiss()
@@ -245,6 +275,62 @@ class SettingsActivity : AppCompatActivity() {
dialog.window?.setDimAmount(0.8f)
}
+ binding.purgeAnimeDownloads.setOnClickListener {
+ val dialog = AlertDialog.Builder(this, R.style.MyPopup)
+ .setTitle("Purge Anime Downloads")
+ .setMessage("Are you sure you want to purge all anime downloads?")
+ .setPositiveButton("Yes") { dialog, _ ->
+ val downloadsManager = Injekt.get()
+ downloadsManager.purgeDownloads(DownloadedType.Type.ANIME)
+ DownloadService.sendRemoveAllDownloads(
+ this,
+ ExoplayerDownloadService::class.java,
+ false
+ )
+ dialog.dismiss()
+ }
+ .setNegativeButton("No") { dialog, _ ->
+ dialog.dismiss()
+ }
+ .create()
+ dialog.window?.setDimAmount(0.8f)
+ dialog.show()
+ }
+
+ binding.purgeMangaDownloads.setOnClickListener {
+ val dialog = AlertDialog.Builder(this, R.style.MyPopup)
+ .setTitle("Purge Manga Downloads")
+ .setMessage("Are you sure you want to purge all manga downloads?")
+ .setPositiveButton("Yes") { dialog, _ ->
+ val downloadsManager = Injekt.get()
+ downloadsManager.purgeDownloads(DownloadedType.Type.MANGA)
+ dialog.dismiss()
+ }
+ .setNegativeButton("No") { dialog, _ ->
+ dialog.dismiss()
+ }
+ .create()
+ dialog.window?.setDimAmount(0.8f)
+ dialog.show()
+ }
+
+ binding.purgeNovelDownloads.setOnClickListener {
+ val dialog = AlertDialog.Builder(this, R.style.MyPopup)
+ .setTitle("Purge Novel Downloads")
+ .setMessage("Are you sure you want to purge all novel downloads?")
+ .setPositiveButton("Yes") { dialog, _ ->
+ val downloadsManager = Injekt.get