From 720b40afa7fcdd6c71f79d55a64ee6d659abef18 Mon Sep 17 00:00:00 2001
From: rebel onion <87634197+rebelonion@users.noreply.github.com>
Date: Thu, 4 Apr 2024 04:03:45 -0500
Subject: [PATCH] feat: custom downloader and downloader location (#313)
* feat: custom downloader (novel broken)
* fix: send headers to ffmpeg
ffmpeg can be a real bitch to work with
* fix: offline page for new download system
* feat: novel to new system | load freezing
* chore: clean manifest
* fix: notification incrementing
* feat: changing the downloads dir
---
app/build.gradle | 12 +
app/src/main/AndroidManifest.xml | 10 -
app/src/main/java/ani/dantotsu/Functions.kt | 36 +-
.../main/java/ani/dantotsu/MainActivity.kt | 8 +-
app/src/main/java/ani/dantotsu/Network.kt | 3 +-
.../ani/dantotsu/download/DownloadsManager.kt | 332 ++++++++--------
.../download/anime/AnimeDownloaderService.kt | 369 +++++++++++-------
.../download/anime/OfflineAnimeFragment.kt | 134 ++++---
.../download/manga/MangaDownloaderService.kt | 69 ++--
.../download/manga/OfflineMangaFragment.kt | 135 ++++---
.../download/novel/NovelDownloaderService.kt | 72 ++--
.../video/ExoplayerDownloadService.kt | 37 --
.../ani/dantotsu/download/video/Helper.kt | 168 +-------
.../dantotsu/media/MediaDetailsActivity.kt | 9 +-
.../ani/dantotsu/media/SubtitleDownloader.kt | 20 +-
.../media/anime/AnimeWatchFragment.kt | 49 ++-
.../dantotsu/media/anime/EpisodeAdapters.kt | 26 +-
.../ani/dantotsu/media/anime/ExoplayerView.kt | 122 +++---
.../dantotsu/media/manga/MangaReadFragment.kt | 106 +++--
.../manga/mangareader/BaseImageAdapter.kt | 6 +
.../dantotsu/media/novel/NovelReadFragment.kt | 35 +-
.../comment/CommentNotificationTask.kt | 6 +-
.../main/java/ani/dantotsu/others/Download.kt | 56 +--
.../ani/dantotsu/others/ImageViewDialog.kt | 2 +-
.../dantotsu/parsers/OfflineAnimeParser.kt | 30 +-
.../dantotsu/parsers/OfflineMangaParser.kt | 28 +-
.../dantotsu/parsers/OfflineNovelParser.kt | 33 +-
.../ani/dantotsu/settings/SettingsActivity.kt | 69 +++-
.../dantotsu/settings/saving/Preferences.kt | 2 +-
.../ani/dantotsu/util/StoragePermissions.kt | 132 +++++++
.../animesource/online/AnimeHttpSource.kt | 3 +-
.../kanade/tachiyomi/network/NetworkHelper.kt | 4 +-
.../tachiyomi/source/online/HttpSource.kt | 3 +-
.../res/layout/activity_settings_common.xml | 45 ++-
app/src/main/res/values/strings.xml | 9 +-
35 files changed, 1162 insertions(+), 1018 deletions(-)
delete mode 100644 app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt
create mode 100644 app/src/main/java/ani/dantotsu/util/StoragePermissions.kt
diff --git a/app/build.gradle b/app/build.gradle
index 12fc9b90..5f6fe8cd 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,6 +21,14 @@ android {
versionName "3.0.0"
versionCode 300000000
signingConfig signingConfigs.debug
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
+ universalApk true
+ }
+ }
}
flavorDimensions += "store"
@@ -99,6 +107,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.10.0'
+ implementation "com.anggrayudi:storage:1.5.5"
// Glide
ext.glide_version = '4.16.0'
@@ -149,6 +158,9 @@ dependencies {
// String Matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
+ implementation group: 'com.arthenica', name: 'ffmpeg-kit-full-gpl', version: '6.0-2.LTS'
+ //implementation 'com.github.yausername.youtubedl-android:library:0.15.0'
+
// Aniyomi
implementation 'io.reactivex:rxjava:1.3.8'
implementation 'io.reactivex:rxandroid:1.2.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e3ff41fa..934bd7b3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -370,16 +370,6 @@
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
-
-
-
-
-
-
-
(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
- val glideUrl = GlideUrl(file.url) { file.headers }
- Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
- .into(this)
+ if (file.url.startsWith("content://")) {
+ Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
+ .override(size).into(this)
+ } else {
+ val glideUrl = GlideUrl(file.url) { file.headers }
+ Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
+ .into(this)
+ }
}
}
}
@@ -877,31 +882,6 @@ fun savePrefs(
}
}
-fun downloadsPermission(activity: AppCompatActivity): Boolean {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
- val permissions = arrayOf(
- Manifest.permission.WRITE_EXTERNAL_STORAGE,
- Manifest.permission.READ_EXTERNAL_STORAGE
- )
-
- val requiredPermissions = permissions.filter {
- ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
- }.toTypedArray()
-
- return if (requiredPermissions.isNotEmpty()) {
- ActivityCompat.requestPermissions(
- activity,
- requiredPermissions,
- DOWNLOADS_PERMISSION_REQUEST_CODE
- )
- false
- } else {
- true
- }
-}
-
-private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
-
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
val contentUri = FileProvider.getUriForFile(
diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt
index adf1334f..08f6aac6 100644
--- a/app/src/main/java/ani/dantotsu/MainActivity.kt
+++ b/app/src/main/java/ani/dantotsu/MainActivity.kt
@@ -3,6 +3,7 @@ package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
+import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Animatable
@@ -15,12 +16,11 @@ import android.os.Looper
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
-import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.TextView
-import android.widget.Toast
import androidx.activity.addCallback
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
@@ -449,7 +449,7 @@ class MainActivity : AppCompatActivity() {
}
}
}
- lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
+ /*lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
@@ -458,7 +458,7 @@ class MainActivity : AppCompatActivity() {
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
}
}
- }
+ }*/ //TODO: remove this
}
override fun onRestart() {
diff --git a/app/src/main/java/ani/dantotsu/Network.kt b/app/src/main/java/ani/dantotsu/Network.kt
index 91840b3e..1c26b3bb 100644
--- a/app/src/main/java/ani/dantotsu/Network.kt
+++ b/app/src/main/java/ani/dantotsu/Network.kt
@@ -9,6 +9,7 @@ import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns
import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
@@ -40,7 +41,7 @@ fun initializeNetwork() {
defaultHeaders = mapOf(
"User-Agent" to
- Injekt.get().defaultUserAgentProvider()
+ defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL)
)
diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
index 4eb7f5a3..fa01cc79 100644
--- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
+++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
@@ -1,14 +1,25 @@
package ani.dantotsu.download
import android.content.Context
-import android.os.Environment
-import android.widget.Toast
+import android.net.Uri
+import androidx.documentfile.provider.DocumentFile
+import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.media.MediaType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
+import ani.dantotsu.snackString
+import ani.dantotsu.util.Logger
+import com.anggrayudi.storage.callback.FolderCallback
+import com.anggrayudi.storage.file.deleteRecursively
+import com.anggrayudi.storage.file.findFolder
+import com.anggrayudi.storage.file.moveFileTo
+import com.anggrayudi.storage.file.moveFolderTo
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
-import java.io.File
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.io.Serializable
class DownloadsManager(private val context: Context) {
@@ -42,27 +53,29 @@ class DownloadsManager(private val context: Context) {
saveDownloads()
}
- fun removeDownload(downloadedType: DownloadedType) {
+ fun removeDownload(downloadedType: DownloadedType, onFinished: () -> Unit) {
downloadsList.remove(downloadedType)
- removeDirectory(downloadedType)
+ CoroutineScope(Dispatchers.IO).launch {
+ removeDirectory(downloadedType)
+ withContext(Dispatchers.Main) {
+ onFinished()
+ }
+ }
saveDownloads()
}
fun removeMedia(title: String, type: MediaType) {
- val subDirectory = type.asText()
- val directory = File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/$subDirectory/$title"
- )
- if (directory.exists()) {
- val deleted = directory.deleteRecursively()
+ val baseDirectory = getBaseDirectory(context, type)
+ val directory = baseDirectory?.findFolder(title)
+ if (directory?.exists() == true) {
+ val deleted = directory.deleteRecursively(context, false)
if (deleted) {
- Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
+ snackString("Successfully deleted")
} else {
- Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
+ snackString("Failed to delete directory")
}
} else {
- Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
+ snackString("Directory does not exist")
cleanDownloads()
}
when (type) {
@@ -89,23 +102,17 @@ class DownloadsManager(private val context: Context) {
private fun cleanDownload(type: MediaType) {
// remove all folders that are not in the downloads list
- val subDirectory = type.asText()
- val directory = File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/$subDirectory"
- )
+ val directory = getBaseDirectory(context, type)
val downloadsSubLists = when (type) {
MediaType.MANGA -> mangaDownloadedTypes
MediaType.ANIME -> animeDownloadedTypes
else -> novelDownloadedTypes
}
- if (directory.exists()) {
+ if (directory?.exists() == true && directory.isDirectory) {
val files = directory.listFiles()
- if (files != null) {
- for (file in files) {
- if (!downloadsSubLists.any { it.title == file.name }) {
- file.deleteRecursively()
- }
+ for (file in files) {
+ if (!downloadsSubLists.any { it.title == file.name }) {
+ file.deleteRecursively(context, false)
}
}
}
@@ -113,27 +120,57 @@ class DownloadsManager(private val context: Context) {
val iterator = downloadsList.iterator()
while (iterator.hasNext()) {
val download = iterator.next()
- val downloadDir = File(directory, download.title)
- if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
+ val downloadDir = directory?.findFolder(download.title)
+ if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
iterator.remove()
}
}
}
- fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List) //for debugging
- {
- val jsonString = gson.toJson(downloadsList)
- val file = File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/downloads.json"
- )
- if (file.parentFile?.exists() == false) {
- file.parentFile?.mkdirs()
+ fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) {
+ try {
+ if (oldUri == newUri) {
+ finished(false, "Source and destination are the same")
+ return
+ }
+ CoroutineScope(Dispatchers.IO).launch {
+
+ val oldBase =
+ DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
+ val newBase =
+ DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
+ val folder =
+ oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
+ folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object:
+ FolderCallback() {
+ override fun onFailed(errorCode: ErrorCode) {
+ when (errorCode) {
+ ErrorCode.CANCELED -> finished(false, "Move canceled")
+ ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(false, "Cannot create file in target")
+ ErrorCode.INVALID_TARGET_FOLDER -> finished(true, "Invalid target folder") // seems to still work
+ ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(false, "No space left on target path")
+ ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
+ ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(false, "Source folder not found")
+ ErrorCode.STORAGE_PERMISSION_DENIED -> finished(false, "Storage permission denied")
+ ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(false, "Target folder cannot have same path with source folder")
+ else -> finished(false, "Failed to move downloads: $errorCode")
+ }
+ Logger.log("Failed to move downloads: $errorCode")
+ super.onFailed(errorCode)
+ }
+
+ override fun onCompleted(result: Result) {
+ finished(true, "Successfully moved downloads")
+ super.onCompleted(result)
+ }
+ })
+ }
+
+ } catch (e: Exception) {
+ snackString("Error: ${e.message}")
+ finished(false, "Failed to move downloads: ${e.message}")
+ return
}
- if (!file.exists()) {
- file.createNewFile()
- }
- file.writeText(jsonString)
}
fun queryDownload(downloadedType: DownloadedType): Boolean {
@@ -149,98 +186,35 @@ class DownloadsManager(private val context: Context) {
}
private fun removeDirectory(downloadedType: DownloadedType) {
- val directory = when (downloadedType.type) {
- MediaType.MANGA -> {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
- )
- }
- MediaType.ANIME -> {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
- )
- }
- else -> {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
- )
- }
- }
+ val baseDirectory = getBaseDirectory(context, downloadedType.type)
+ val directory =
+ baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
// Check if the directory exists and delete it recursively
- if (directory.exists()) {
- val deleted = directory.deleteRecursively()
+ if (directory?.exists() == true) {
+ val deleted = directory.deleteRecursively(context, false)
if (deleted) {
- Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
- }
- } else {
- Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
- }
- }
+ snackString("Successfully deleted")
- fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
- val directory = when (downloadedType.type) {
- MediaType.MANGA -> {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
- )
- }
- MediaType.ANIME -> {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
- )
- }
- else -> {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
- )
- }
- }
- val destination = File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
- )
- if (directory.exists()) {
- val copied = directory.copyRecursively(destination, true)
- if (copied) {
- Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
} else {
- Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
+ snackString("Failed to delete directory")
}
} else {
- Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
+ snackString("Directory does not exist")
}
}
fun purgeDownloads(type: MediaType) {
- val directory = when (type) {
- MediaType.MANGA -> {
- File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
- }
- MediaType.ANIME -> {
- File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
- }
- else -> {
- File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
- }
- }
- if (directory.exists()) {
- val deleted = directory.deleteRecursively()
+ val directory = getBaseDirectory(context, type)
+ if (directory?.exists() == true) {
+ val deleted = directory.deleteRecursively(context, false)
if (deleted) {
- Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
+ snackString("Successfully deleted")
} else {
- Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
+ snackString("Failed to delete directory")
}
} else {
- Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
+ snackString("Directory does not exist")
}
downloadsList.removeAll { it.type == type }
@@ -248,59 +222,95 @@ class DownloadsManager(private val context: Context) {
}
companion object {
- const val novelLocation = "Dantotsu/Novel"
- const val mangaLocation = "Dantotsu/Manga"
- const val animeLocation = "Dantotsu/Anime"
+ private const val BASE_LOCATION = "Dantotsu"
+ private const val MANGA_SUB_LOCATION = "Manga"
+ private const val ANIME_SUB_LOCATION = "Anime"
+ private const val NOVEL_SUB_LOCATION = "Novel"
+ private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
- fun getDirectory(
- context: Context,
- type: MediaType,
- title: String,
- chapter: String? = null
- ): File {
+ fun String?.findValidName(): String {
+ return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
+ }
+
+ /**
+ * Get and create a base directory for the given type
+ * @param context the context
+ * @param type the type of media
+ * @return the base directory
+ */
+
+ private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
+ val baseDirectory = Uri.parse(PrefManager.getVal(PrefName.DownloadsDir))
+ if (baseDirectory == Uri.EMPTY) return null
+ var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
+ base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
return when (type) {
MediaType.MANGA -> {
- if (chapter != null) {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "$mangaLocation/$title/$chapter"
- )
- } else {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "$mangaLocation/$title"
- )
- }
+ base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
}
+
MediaType.ANIME -> {
- if (chapter != null) {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "$animeLocation/$title/$chapter"
- )
- } else {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "$animeLocation/$title"
- )
- }
+ base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
}
+
else -> {
- if (chapter != null) {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "$novelLocation/$title/$chapter"
- )
- } else {
- File(
- context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "$novelLocation/$title"
- )
- }
+ base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
}
}
}
+
+ /**
+ * Get and create a subdirectory for the given type
+ * @param context the context
+ * @param type the type of media
+ * @param title the title of the media
+ * @param chapter the chapter of the media
+ * @return the subdirectory
+ */
+ fun getSubDirectory(
+ context: Context,
+ type: MediaType,
+ overwrite: Boolean,
+ title: String,
+ chapter: String? = null
+ ): DocumentFile? {
+ val baseDirectory = getBaseDirectory(context, type) ?: return null
+ return if (chapter != null) {
+ baseDirectory.findOrCreateFolder(title, false)
+ ?.findOrCreateFolder(chapter, overwrite)
+ } else {
+ baseDirectory.findOrCreateFolder(title, overwrite)
+ }
+ }
+
+ fun getDirSize(context: Context, type: MediaType, title: String, chapter: String? = null): Long {
+ val directory = getSubDirectory(context, type, false, title, chapter) ?: return 0
+ var size = 0L
+ directory.listFiles().forEach {
+ size += it.length()
+ }
+ return size
+ }
+
+ private fun DocumentFile.findOrCreateFolder(
+ name: String, overwrite: Boolean
+ ): DocumentFile? {
+ return if (overwrite) {
+ findFolder(name.findValidName())?.delete()
+ createDirectory(name.findValidName())
+ } else {
+ findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
+ }
+ }
+
}
}
-data class DownloadedType(val title: String, val chapter: String, val type: MediaType) : Serializable
+data class DownloadedType(
+ val pTitle: String, val pChapter: String, val type: MediaType
+) : Serializable {
+ val title: String
+ get() = pTitle.findValidName()
+ val chapter: String
+ get() = pChapter.findValidName()
+}
diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
index 5d9c7a5b..daaa200e 100644
--- a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
@@ -9,24 +9,21 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
-import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
+import androidx.documentfile.provider.DocumentFile
import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.offline.DownloadManager
-import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
-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.download.DownloadsManager.Companion.getSubDirectory
+import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
@@ -36,6 +33,12 @@ import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
+import com.anggrayudi.storage.file.forceDelete
+import com.anggrayudi.storage.file.openOutputStream
+import com.arthenica.ffmpegkit.FFmpegKit
+import com.arthenica.ffmpegkit.FFmpegKitConfig
+import com.arthenica.ffmpegkit.FFprobeKit
+import com.arthenica.ffmpegkit.SessionState
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
@@ -46,7 +49,6 @@ 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.Job
import kotlinx.coroutines.SupervisorJob
@@ -56,13 +58,12 @@ 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
@@ -88,6 +89,7 @@ class AnimeDownloaderService : Service() {
setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
+ setProgress(100, 0, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
@@ -156,27 +158,14 @@ class AnimeDownloaderService : Service() {
@UnstableApi
fun cancelDownload(taskName: String) {
- val url =
- AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
- ?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
- if (url.isEmpty()) {
- snackString("Failed to cancel download")
- return
+ val sessionIds =
+ AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
+ .map { it.sessionId }.toMutableList()
+ sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
+ sessionIds.forEach {
+ FFmpegKit.cancel(it)
}
currentTasks.removeAll { it.getTaskName() == taskName }
- DownloadService.sendSetStopReason(
- this@AnimeDownloaderService,
- ExoplayerDownloadService::class.java,
- url,
- androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
- false
- )
- DownloadService.sendRemoveDownload(
- this@AnimeDownloaderService,
- ExoplayerDownloadService::class.java,
- url,
- false
- )
CoroutineScope(Dispatchers.Default).launch {
mutex.withLock {
downloadJobs[taskName]?.cancel()
@@ -209,7 +198,7 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
try {
- val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
+ //val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
@@ -220,18 +209,80 @@ class AnimeDownloaderService : Service() {
true
}
- builder.setContentText("Downloading ${task.title} - ${task.episode}")
+ builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
- currActivity()?.let {
- Helper.downloadVideo(
- it,
- task.video,
- task.subtitle
- )
+ val outputDir = getSubDirectory(
+ this@AnimeDownloaderService,
+ MediaType.ANIME,
+ false,
+ task.title,
+ task.episode
+ ) ?: throw Exception("Failed to create output directory")
+
+ outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
+ val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
+ ?: throw Exception("Failed to create output file")
+
+ var percent = 0
+ var totalLength = 0.0
+ val path = FFmpegKitConfig.getSafParameterForWrite(
+ this@AnimeDownloaderService,
+ outputFile.uri
+ )
+ val headersStringBuilder = StringBuilder().append(" ")
+ task.video.file.headers.forEach {
+ headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
}
+ headersStringBuilder.append(" ")
+ FFprobeKit.executeAsync(
+ "-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\"",
+ {
+ Logger.log("FFprobeKit: $it")
+ }, {
+ if (it.message.toDoubleOrNull() != null) {
+ totalLength = it.message.toDouble()
+ }
+ })
+
+ var request = "-headers"
+ val headers = headersStringBuilder.toString()
+ if (task.video.file.headers.isNotEmpty()) {
+ request += headers
+ }
+ request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
+ println("Request: $request")
+ val ffTask =
+ FFmpegKit.executeAsync(request,
+ { session ->
+ val state: SessionState = session.state
+ val returnCode = session.returnCode
+ // CALLED WHEN SESSION IS EXECUTED
+ Logger.log(
+ java.lang.String.format(
+ "FFmpeg process exited with state %s and rc %s.%s",
+ state,
+ returnCode,
+ session.failStackTrace
+ )
+ )
+
+ }, {
+ // CALLED WHEN SESSION PRINTS LOGS
+ Logger.log(it.message)
+ }) {
+ // CALLED WHEN SESSION GENERATES STATISTICS
+ val timeInMilliseconds = it.time
+ if (timeInMilliseconds > 0 && totalLength > 0) {
+ percent = ((it.time / 1000) / totalLength * 100).toInt()
+ }
+ Logger.log("Statistics: $it")
+ }
+ task.sessionId = ffTask.sessionId
+ currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
+ ffTask.sessionId
saveMediaInfo(task)
task.subtitle?.let {
@@ -245,86 +296,115 @@ class AnimeDownloaderService : Service() {
)
)
}
- val downloadStarted =
- hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
-
- if (!downloadStarted) {
- Logger.log("Download failed to start")
- builder.setContentText("${task.title} - ${task.episode} Download failed to start")
- notificationManager.notify(NOTIFICATION_ID, builder.build())
- snackString("${task.title} - ${task.episode} Download failed to start")
- broadcastDownloadFailed(task.episode)
- return@withContext
- }
-
// periodically check if the download is complete
- while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
- val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
- if (download != null) {
- if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
- Logger.log("Download failed")
- builder.setContentText("${task.title} - ${task.episode} Download failed")
- notificationManager.notify(NOTIFICATION_ID, builder.build())
- snackString("${task.title} - ${task.episode} Download failed")
- Logger.log("Download failed: ${download.failureReason}")
- downloadsManager.removeDownload(
- DownloadedType(
+ while (ffTask.state != SessionState.COMPLETED) {
+ if (ffTask.state == SessionState.FAILED) {
+ Logger.log("Download failed")
+ builder.setContentText(
+ "${
+ getTaskName(
task.title,
- task.episode,
- MediaType.ANIME,
+ task.episode
)
- )
- Injekt.get().logException(
- Exception(
- "Anime Download failed:" +
- " ${download.failureReason}" +
- " url: ${task.video.file.url}" +
- " title: ${task.title}" +
- " episode: ${task.episode}"
- )
- )
- currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
- broadcastDownloadFailed(task.episode)
- break
- }
- if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
- Logger.log("Download completed")
- builder.setContentText("${task.title} - ${task.episode} Download completed")
- notificationManager.notify(NOTIFICATION_ID, builder.build())
- snackString("${task.title} - ${task.episode} Download completed")
- PrefManager.getAnimeDownloadPreferences().edit().putString(
- task.getTaskName(),
- task.video.file.url
- ).apply()
- downloadsManager.addDownload(
- DownloadedType(
- task.title,
- task.episode,
- MediaType.ANIME,
- )
- )
- currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
- broadcastDownloadFinished(task.episode)
- break
- }
- if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
- Logger.log("Download stopped")
- builder.setContentText("${task.title} - ${task.episode} Download stopped")
- notificationManager.notify(NOTIFICATION_ID, builder.build())
- snackString("${task.title} - ${task.episode} Download stopped")
- break
- }
- broadcastDownloadProgress(
- task.episode,
- download.percentDownloaded.toInt()
+ } Download failed"
)
- if (notifi) {
- notificationManager.notify(NOTIFICATION_ID, builder.build())
- }
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ snackString("${getTaskName(task.title, task.episode)} Download failed")
+ Logger.log("Download failed: ${ffTask.failStackTrace}")
+ downloadsManager.removeDownload(
+ DownloadedType(
+ task.title,
+ task.episode,
+ MediaType.ANIME,
+ )
+ ) {}
+ Injekt.get().logException(
+ Exception(
+ "Anime Download failed:" +
+ " ${getTaskName(task.title, task.episode)}" +
+ " url: ${task.video.file.url}" +
+ " title: ${task.title}" +
+ " episode: ${task.episode}"
+ )
+ )
+ currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
+ broadcastDownloadFailed(task.episode)
+ break
+ }
+ builder.setProgress(
+ 100, percent.coerceAtMost(99),
+ false
+ )
+ broadcastDownloadProgress(
+ task.episode,
+ percent.coerceAtMost(99)
+ )
+ if (notifi) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
}
kotlinx.coroutines.delay(2000)
}
+ if (ffTask.state == SessionState.COMPLETED) {
+ if (ffTask.returnCode.isValueError) {
+ Logger.log("Download failed")
+ builder.setContentText(
+ "${
+ getTaskName(
+ task.title,
+ task.episode
+ )
+ } Download failed"
+ )
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ snackString("${getTaskName(task.title, task.episode)} Download failed")
+ downloadsManager.removeDownload(
+ DownloadedType(
+ task.title,
+ task.episode,
+ MediaType.ANIME,
+ )
+ ) {}
+ Injekt.get().logException(
+ Exception(
+ "Anime Download failed:" +
+ " ${getTaskName(task.title, task.episode)}" +
+ " url: ${task.video.file.url}" +
+ " title: ${task.title}" +
+ " episode: ${task.episode}"
+ )
+ )
+ currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
+ broadcastDownloadFailed(task.episode)
+ return@withContext
+ }
+ Logger.log("Download completed")
+ builder.setContentText(
+ "${
+ getTaskName(
+ task.title,
+ task.episode
+ )
+ } Download completed"
+ )
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ snackString("${getTaskName(task.title, task.episode)} Download completed")
+ PrefManager.getAnimeDownloadPreferences().edit().putString(
+ task.getTaskName(),
+ task.video.file.url
+ ).apply()
+ downloadsManager.addDownload(
+ DownloadedType(
+ task.title,
+ task.episode,
+ MediaType.ANIME,
+ )
+ )
+
+ currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
+ broadcastDownloadFinished(task.episode)
+ } else throw Exception("Download failed")
+
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
@@ -337,35 +417,24 @@ class AnimeDownloaderService : Service() {
}
}
- @androidx.annotation.OptIn(UnstableApi::class)
- suspend fun hasDownloadStarted(
- downloadManager: DownloadManager,
- task: AnimeDownloadTask,
- timeout: Long
- ): Boolean {
- val startTime = System.currentTimeMillis()
- while (System.currentTimeMillis() - startTime < timeout) {
- val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
- if (download != null) {
- return true
- }
- // Delay between each poll
- kotlinx.coroutines.delay(500)
- }
- return false
- }
-
- @OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: AnimeDownloadTask) {
CoroutineScope(Dispatchers.IO).launch {
- val directory = File(
- getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "${DownloadsManager.animeLocation}/${task.title}"
- )
- val episodeDirectory = File(directory, task.episode)
- if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
+ val directory =
+ getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
+ ?: throw Exception("Directory not found")
+ directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
+ val file = directory.createFile("application/json", "media.json")
+ ?: throw Exception("File not created")
+ val episodeDirectory =
+ getSubDirectory(
+ this@AnimeDownloaderService,
+ MediaType.ANIME,
+ false,
+ task.title,
+ task.episode
+ )
+ ?: throw Exception("Directory not found")
- val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -399,14 +468,25 @@ class AnimeDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
- file.writeText(jsonString)
+ try {
+ file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
+ if (output == null) throw Exception("Output stream is null")
+ output.write(jsonString.toByteArray())
+ }
+ } catch (e: android.system.ErrnoException) {
+ e.printStackTrace()
+ Toast.makeText(
+ this@AnimeDownloaderService,
+ "Error while saving: ${e.localizedMessage}",
+ Toast.LENGTH_LONG
+ ).show()
+ }
}
}
}
}
-
- private suspend fun downloadImage(url: String, directory: File, name: String): String? =
+ private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -417,13 +497,16 @@ class AnimeDownloaderService : Service() {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
- val file = File(directory, name)
- FileOutputStream(file).use { output ->
+ directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
+ val file =
+ directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
+ file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
+ if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
- return@withContext file.absolutePath
+ return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
@@ -490,14 +573,15 @@ class AnimeDownloaderService : Service() {
val episodeImage: String? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
+ var sessionId: Long = -1
) {
fun getTaskName(): String {
- return "$title - $episode"
+ return "${title.replace("/", "")}/${episode.replace("/", "")}"
}
companion object {
fun getTaskName(title: String, episode: String): String {
- return "$title - $episode"
+ return "${title.replace("/", "")}/${episode.replace("/", "")}"
}
}
}
@@ -511,7 +595,6 @@ class AnimeDownloaderService : Service() {
object AnimeServiceDataSingleton {
var video: Video? = null
- var sourceMedia: Media? = null
var downloadQueue: Queue = ConcurrentLinkedQueue()
@Volatile
diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
index 64329759..6c21cb86 100644
--- a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
+++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
@@ -4,7 +4,6 @@ package ani.dantotsu.download.anime
import android.content.Intent
import android.net.Uri
import android.os.Bundle
-import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
@@ -25,6 +24,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import ani.dantotsu.R
import ani.dantotsu.bottomBar
@@ -33,6 +33,7 @@ import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.initActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -44,6 +45,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
+import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
@@ -55,9 +57,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-import java.io.File
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@@ -66,6 +72,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total: TextView
+ private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -112,10 +119,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
})
var style: Int = PrefManager.getVal(PrefName.OfflineView)
val layoutList = view.findViewById(R.id.downloadedList)
- val layoutcompact = view.findViewById(R.id.downloadedGrid)
+ val layoutCompact = view.findViewById(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
- 1 -> layoutcompact
+ 1 -> layoutCompact
else -> layoutList
}
selected.alpha = 1f
@@ -136,7 +143,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
grid()
}
- layoutcompact.setOnClickListener {
+ layoutCompact.setOnClickListener {
selected(it as ImageView)
style = 1
PrefManager.setVal(PrefName.OfflineView, style)
@@ -156,11 +163,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@OptIn(UnstableApi::class)
private fun grid() {
gridView.visibility = View.VISIBLE
- getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
+ getDownloads()
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
@@ -168,20 +175,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val media =
- downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
+ downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title.findValidName() }
media?.let {
- val mediaModel = getMedia(it)
- if (mediaModel == null) {
- snackString("Error loading media.json")
- return@let
+ lifecycleScope.launch {
+ val mediaModel = getMedia(it)
+ if (mediaModel == null) {
+ snackString("Error loading media.json")
+ return@launch
+ }
+ MediaDetailsActivity.mediaSingleton = mediaModel
+ ContextCompat.startActivity(
+ requireActivity(),
+ Intent(requireContext(), MediaDetailsActivity::class.java)
+ .putExtra("download", true),
+ null
+ )
}
- MediaDetailsActivity.mediaSingleton = mediaModel
- ContextCompat.startActivity(
- requireActivity(),
- Intent(requireContext(), MediaDetailsActivity::class.java)
- .putExtra("download", true),
- null
- )
} ?: run {
snackString("no media found")
}
@@ -204,13 +213,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
}
- for (mediaId in mediaIds) {
- ani.dantotsu.download.video.Helper.downloadManager(requireContext())
- .removeDownload(mediaId.toString())
- }
getDownloads()
- adapter.setItems(downloads)
- total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
@@ -238,7 +241,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
- // Implement behavior for different scroll states if needed
}
override fun onScroll(
@@ -261,7 +263,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
- adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -281,25 +282,39 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private fun getDownloads() {
downloads = listOf()
- val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
- val newAnimeDownloads = mutableListOf()
- for (title in animeTitles) {
- val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
- val download = tDownloads.first()
- val offlineAnimeModel = loadOfflineAnimeModel(download)
- newAnimeDownloads += offlineAnimeModel
+ if (downloadsJob.isActive) {
+ downloadsJob.cancel()
+ }
+ downloadsJob = Job()
+ CoroutineScope(Dispatchers.IO + downloadsJob).launch {
+ val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
+ val newAnimeDownloads = mutableListOf()
+ for (title in animeTitles) {
+ val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
+ val download = tDownloads.first()
+ val offlineAnimeModel = loadOfflineAnimeModel(download)
+ newAnimeDownloads += offlineAnimeModel
+ }
+ downloads = newAnimeDownloads
+ withContext(Dispatchers.Main) {
+ adapter.setItems(downloads)
+ total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
+ adapter.notifyDataSetChanged()
+ }
}
- downloads = newAnimeDownloads
}
- private fun getMedia(downloadedType: DownloadedType): Media? {
- val type = downloadedType.type.asText()
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/$type/${downloadedType.title}"
- )
- //load media.json and convert to media class with gson
+ /**
+ * Load media.json file from the directory and convert it to Media class
+ * @param downloadedType DownloadedType object
+ * @return Media object
+ */
+ private suspend fun getMedia(downloadedType: DownloadedType): Media? {
return try {
+ val directory = DownloadsManager.getSubDirectory(
+ context ?: currContext()!!, downloadedType.type,
+ false, downloadedType.title
+ )
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -311,8 +326,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
- val media = File(directory, "media.json")
- val mediaJson = media.readText()
+ val media = directory?.findFile("media.json")
+ ?: return null
+ val mediaJson =
+ media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
+ it?.readText()
+ }
+ ?: return null
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
@@ -322,22 +342,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
}
}
- private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
+ /**
+ * Load OfflineAnimeModel from the directory
+ * @param downloadedType DownloadedType object
+ * @return OfflineAnimeModel object
+ */
+ private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val type = downloadedType.type.asText()
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/$type/${downloadedType.title}"
- )
- //load media.json and convert to media class with gson
try {
+ val directory = DownloadsManager.getSubDirectory(
+ context ?: currContext()!!, downloadedType.type,
+ false, downloadedType.title
+ )
val mediaModel = getMedia(downloadedType)!!
- val cover = File(directory, "cover.jpg")
- val coverUri: Uri? = if (cover.exists()) {
- Uri.fromFile(cover)
+ val cover = directory?.findFile("cover.jpg")
+ val coverUri: Uri? = if (cover?.exists() == true) {
+ cover.uri
} else null
- val banner = File(directory, "banner.jpg")
- val bannerUri: Uri? = if (banner.exists()) {
- Uri.fromFile(banner)
+ val banner = directory?.findFile("banner.jpg")
+ val bannerUri: Uri? = if (banner?.exists() == true) {
+ banner.uri
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
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 92e811bd..08ddb3a7 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
@@ -10,17 +10,18 @@ import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.os.Build
-import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
+import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.manga.ImageData
@@ -31,6 +32,9 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STAR
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
+import com.anggrayudi.storage.file.deleteRecursively
+import com.anggrayudi.storage.file.forceDelete
+import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
@@ -51,8 +55,6 @@ import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-import java.io.File
-import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
@@ -189,13 +191,20 @@ class MangaDownloaderService : Service() {
true
}
- //val deferredList = mutableListOf>()
val deferredMap = mutableMapOf>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
+ getSubDirectory(
+ this@MangaDownloaderService,
+ MediaType.MANGA,
+ false,
+ task.title,
+ task.chapter
+ )?.deleteRecursively(this@MangaDownloaderService)
+
// Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
@@ -263,24 +272,18 @@ class MangaDownloaderService : Service() {
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try {
// Define the directory within the private external storage space
- val directory = File(
- this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Manga/$title/$chapter"
- )
-
- if (!directory.exists()) {
- directory.mkdirs()
- }
-
- // Create a file reference within that directory for your image
- val file = File(directory, fileName)
+ val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
+ ?: throw Exception("Directory not found")
+ directory.findFile(fileName)?.forceDelete(this)
+ // Create a file reference within that directory for the image
+ val file =
+ directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
// Use a FileOutputStream to write the bitmap to the file
- FileOutputStream(file).use { outputStream ->
+ file.openOutputStream(this, false).use { outputStream ->
+ if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}
-
-
} catch (e: Exception) {
println("Exception while saving image: ${e.message}")
snackString("Exception while saving image: ${e.message}")
@@ -291,13 +294,12 @@ class MangaDownloaderService : Service() {
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) {
launchIO {
- val directory = File(
- getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Manga/${task.title}"
- )
- if (!directory.exists()) directory.mkdirs()
-
- val file = File(directory, "media.json")
+ val directory =
+ getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
+ ?: throw Exception("Directory not found")
+ directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
+ val file = directory.createFile("application/json", "media.json")
+ ?: throw Exception("File not created")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -312,7 +314,10 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
- file.writeText(jsonString)
+ file.openOutputStream(this@MangaDownloaderService, false).use { output ->
+ if (output == null) throw Exception("Output stream is null")
+ output.write(jsonString.toByteArray())
+ }
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
@@ -327,7 +332,7 @@ class MangaDownloaderService : Service() {
}
- private suspend fun downloadImage(url: String, directory: File, name: String): String? =
+ private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -337,14 +342,16 @@ class MangaDownloaderService : Service() {
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
-
- val file = File(directory, name)
- FileOutputStream(file).use { output ->
+ directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
+ val file =
+ directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
+ file.openOutputStream(this@MangaDownloaderService, false).use { output ->
+ if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
- return@withContext file.absolutePath
+ return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
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 99250edf..1d912887 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
@@ -3,7 +3,6 @@ package ani.dantotsu.download.manga
import android.content.Intent
import android.net.Uri
import android.os.Bundle
-import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
@@ -23,6 +22,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
@@ -30,6 +30,7 @@ import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.initActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -41,6 +42,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
+import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
@@ -48,9 +50,13 @@ import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-import java.io.File
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
@@ -59,6 +65,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
private lateinit var total: TextView
+ private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -148,11 +155,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun grid() {
gridView.visibility = View.VISIBLE
- getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
+ getDownloads()
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text =
@@ -164,14 +171,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
-
- ContextCompat.startActivity(
- requireActivity(),
- Intent(requireContext(), MediaDetailsActivity::class.java)
- .putExtra("media", getMedia(it))
- .putExtra("download", true),
- null
- )
+ lifecycleScope.launch {
+ ContextCompat.startActivity(
+ requireActivity(),
+ Intent(requireContext(), MediaDetailsActivity::class.java)
+ .putExtra("media", getMedia(it))
+ .putExtra("download", true),
+ null
+ )
+ }
} ?: run {
snackString("no media found")
}
@@ -194,9 +202,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
getDownloads()
- adapter.setItems(downloads)
- total.text =
- if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
@@ -225,7 +230,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
- // Implement behavior for different scroll states if needed
}
override fun onScroll(
@@ -248,7 +252,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
- adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -268,42 +271,62 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() {
downloads = listOf()
- val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
- val newMangaDownloads = mutableListOf()
- for (title in mangaTitles) {
- val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
- val download = tDownloads.first()
- val offlineMangaModel = loadOfflineMangaModel(download)
- newMangaDownloads += offlineMangaModel
+ if (downloadsJob.isActive) {
+ downloadsJob.cancel()
}
- downloads = newMangaDownloads
- val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
- val newNovelDownloads = mutableListOf()
- for (title in novelTitles) {
- val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
- val download = tDownloads.first()
- val offlineMangaModel = loadOfflineMangaModel(download)
- newNovelDownloads += offlineMangaModel
+ downloads = listOf()
+ downloadsJob = Job()
+ CoroutineScope(Dispatchers.IO + downloadsJob).launch {
+ val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
+ val newMangaDownloads = mutableListOf()
+ for (title in mangaTitles) {
+ val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
+ val download = tDownloads.first()
+ val offlineMangaModel = loadOfflineMangaModel(download)
+ newMangaDownloads += offlineMangaModel
+ }
+ downloads = newMangaDownloads
+ val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
+ val newNovelDownloads = mutableListOf()
+ for (title in novelTitles) {
+ val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
+ val download = tDownloads.first()
+ val offlineMangaModel = loadOfflineMangaModel(download)
+ newNovelDownloads += offlineMangaModel
+ }
+ downloads += newNovelDownloads
+ withContext(Dispatchers.Main) {
+ adapter.setItems(downloads)
+ total.text =
+ if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
+ adapter.notifyDataSetChanged()
+ }
}
- downloads += newNovelDownloads
}
- private fun getMedia(downloadedType: DownloadedType): Media? {
- val type = downloadedType.type.asText()
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/$type/${downloadedType.title}"
- )
- //load media.json and convert to media class with gson
+ /**
+ * Load media.json file from the directory and convert it to Media class
+ * @param downloadedType DownloadedType object
+ * @return Media object
+ */
+ private suspend fun getMedia(downloadedType: DownloadedType): Media? {
return try {
+ val directory = getSubDirectory(
+ context ?: currContext()!!, downloadedType.type,
+ false, downloadedType.title
+ )
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
- val media = File(directory, "media.json")
- val mediaJson = media.readText()
+ val media = directory?.findFile("media.json")
+ ?: return null
+ val mediaJson =
+ media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
+ it?.readText()
+ }
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
@@ -313,22 +336,22 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
- private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
+ private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = downloadedType.type.asText()
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/$type/${downloadedType.title}"
- )
//load media.json and convert to media class with gson
try {
+ val directory = getSubDirectory(
+ context ?: currContext()!!, downloadedType.type,
+ false, downloadedType.title
+ )
val mediaModel = getMedia(downloadedType)!!
- val cover = File(directory, "cover.jpg")
- val coverUri: Uri? = if (cover.exists()) {
- Uri.fromFile(cover)
+ val cover = directory?.findFile("cover.jpg")
+ val coverUri: Uri? = if (cover?.exists() == true) {
+ cover.uri
} else null
- val banner = File(directory, "banner.jpg")
- val bannerUri: Uri? = if (banner.exists()) {
- Uri.fromFile(banner)
+ val banner = directory?.findFile("banner.jpg")
+ val bannerUri: Uri? = if (banner?.exists() == true) {
+ banner.uri
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
@@ -336,14 +359,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
- val readchapter = (mediaModel.userProgress ?: "~").toString()
- val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
+ val readChapter = (mediaModel.userProgress ?: "~").toString()
+ val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val chapters = " Chapters"
return OfflineMangaModel(
title,
score,
- totalchapter,
- readchapter,
+ totalChapter,
+ readChapter,
type,
chapters,
isOngoing,
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 fc432313..123a54c1 100644
--- a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
@@ -16,15 +16,19 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
+import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
+import com.anggrayudi.storage.file.forceDelete
+import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications
@@ -250,24 +254,25 @@ class NovelDownloaderService : Service() {
if (!response.isSuccessful) {
throw IOException("Failed to download file: ${response.message}")
}
+ val directory = getSubDirectory(
+ this@NovelDownloaderService,
+ MediaType.NOVEL,
+ false,
+ task.title,
+ task.chapter
+ ) ?: throw Exception("Directory not found")
+ directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
- val file = File(
- this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
- )
-
- // Create directories if they don't exist
- file.parentFile?.takeIf { !it.exists() }?.mkdirs()
-
- // Overwrite existing file
- if (file.exists()) file.delete()
+ val file = directory.createFile("application/epub+zip", "0.epub")
+ ?: throw Exception("File not created")
//download cover
task.coverUrl?.let {
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
}
+ val outputStream = this@NovelDownloaderService.contentResolver.openOutputStream(file.uri) ?: throw Exception("Could not open OutputStream")
- val sink = file.sink().buffer()
+ val sink = outputStream.sink().buffer()
val responseBody = response.body
val totalBytes = responseBody.contentLength()
var downloadedBytes = 0L
@@ -352,13 +357,16 @@ class NovelDownloaderService : Service() {
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) {
launchIO {
- val directory = File(
- getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Novel/${task.title}"
- )
- if (!directory.exists()) directory.mkdirs()
-
- val file = File(directory, "media.json")
+ val directory =
+ DownloadsManager.getSubDirectory(
+ this@NovelDownloaderService,
+ MediaType.NOVEL,
+ false,
+ task.title
+ ) ?: throw Exception("Directory not found")
+ directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
+ val file = directory.createFile("application/json", "media.json")
+ ?: throw Exception("File not created")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -372,33 +380,47 @@ class NovelDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
- file.writeText(jsonString)
+ try {
+ file.openOutputStream(this@NovelDownloaderService, false).use { output ->
+ if (output == null) throw Exception("Output stream is null")
+ output.write(jsonString.toByteArray())
+ }
+ } catch (e: android.system.ErrnoException) {
+ e.printStackTrace()
+ Toast.makeText(
+ this@NovelDownloaderService,
+ "Error while saving: ${e.localizedMessage}",
+ Toast.LENGTH_LONG
+ ).show()
+ }
}
}
}
}
- private suspend fun downloadImage(url: String, directory: File, name: String): String? =
+ private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(
Dispatchers.IO
) {
var connection: HttpURLConnection? = null
- println("Downloading url $url")
+ Logger.log("Downloading url $url")
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
-
- val file = File(directory, name)
- FileOutputStream(file).use { output ->
+ directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
+ val file =
+ directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
+ file.openOutputStream(this@NovelDownloaderService, false).use { output ->
+ if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
- return@withContext file.absolutePath
+ return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
diff --git a/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt b/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt
deleted file mode 100644
index 4add998c..00000000
--- a/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package ani.dantotsu.download.video
-
-import android.app.Notification
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.offline.Download
-import androidx.media3.exoplayer.offline.DownloadManager
-import androidx.media3.exoplayer.offline.DownloadNotificationHelper
-import androidx.media3.exoplayer.offline.DownloadService
-import androidx.media3.exoplayer.scheduler.PlatformScheduler
-import androidx.media3.exoplayer.scheduler.Scheduler
-import ani.dantotsu.R
-
-@UnstableApi
-class ExoplayerDownloadService :
- DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
- companion object {
- private const val JOB_ID = 1
- private const val FOREGROUND_NOTIFICATION_ID = 1
- }
-
- override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
-
- override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
-
- override fun getForegroundNotification(
- downloads: MutableList,
- notMetRequirements: Int
- ): Notification =
- DownloadNotificationHelper(this, "download_service").buildProgressNotification(
- this,
- R.drawable.mono,
- null,
- null,
- downloads,
- notMetRequirements
- )
-}
\ No newline at end of file
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 258c123a..ed146b90 100644
--- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt
+++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt
@@ -53,140 +53,6 @@ import java.util.concurrent.Executors
@SuppressLint("UnsafeOptInUsageError")
object Helper {
-
-
- private var simpleCache: SimpleCache? = null
-
- fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
- val dataSourceFactory = DataSource.Factory {
- val dataSource: HttpDataSource =
- OkHttpDataSource.Factory(okHttpClient).createDataSource()
- defaultHeaders.forEach {
- dataSource.setRequestProperty(it.key, it.value)
- }
- video.file.headers.forEach {
- dataSource.setRequestProperty(it.key, it.value)
- }
- dataSource
- }
- val mimeType = when (video.format) {
- VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
- VideoType.DASH -> MimeTypes.APPLICATION_MPD
- else -> MimeTypes.APPLICATION_MP4
- }
-
- val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
- var sub: MediaItem.SubtitleConfiguration? = null
- if (subtitle != null) {
- sub = MediaItem.SubtitleConfiguration
- .Builder(Uri.parse(subtitle.file.url))
- .setSelectionFlags(C.SELECTION_FLAG_FORCED)
- .setMimeType(
- when (subtitle.type) {
- SubtitleType.VTT -> MimeTypes.TEXT_VTT
- SubtitleType.ASS -> MimeTypes.TEXT_SSA
- SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
- SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
- }
- )
- .build()
- }
- if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
- val mediaItem = builder.build()
- val downloadHelper = DownloadHelper.forMediaItem(
- context,
- mediaItem,
- DefaultRenderersFactory(context),
- dataSourceFactory
- )
- downloadHelper.prepare(object : DownloadHelper.Callback {
- override fun onPrepared(helper: DownloadHelper) {
- helper.getDownloadRequest(null).let {
- DownloadService.sendAddDownload(
- context,
- ExoplayerDownloadService::class.java,
- it,
- false
- )
- }
- }
-
- override fun onPrepareError(helper: DownloadHelper, e: IOException) {
- logError(e)
- }
- })
- }
-
-
- private var download: DownloadManager? = null
- private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
-
- @Synchronized
- @UnstableApi
- fun downloadManager(context: Context): DownloadManager {
- return download ?: let {
- val database = Injekt.get()
- val dataSourceFactory = DataSource.Factory {
- //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
- val networkHelper = Injekt.get()
- val okHttpClient = networkHelper.client
- val dataSource: HttpDataSource =
- OkHttpDataSource.Factory(okHttpClient).createDataSource()
- defaultHeaders.forEach {
- dataSource.setRequestProperty(it.key, it.value)
- }
- dataSource
- }
- val threadPoolSize = Runtime.getRuntime().availableProcessors()
- val executorService = Executors.newFixedThreadPool(threadPoolSize)
- val downloadManager = DownloadManager(
- context,
- database,
- getSimpleCache(context),
- dataSourceFactory,
- executorService
- ).apply {
- requirements =
- Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
- maxParallelDownloads = 3
- }
- downloadManager.addListener( //for testing
- object : DownloadManager.Listener {
- override fun onDownloadChanged(
- downloadManager: DownloadManager,
- download: Download,
- finalException: Exception?
- ) {
- when (download.state) {
- Download.STATE_COMPLETED -> Logger.log("Download Completed")
- Download.STATE_FAILED -> Logger.log("Download Failed")
- Download.STATE_STOPPED -> Logger.log("Download Stopped")
- Download.STATE_QUEUED -> Logger.log("Download Queued")
- Download.STATE_DOWNLOADING -> Logger.log("Download Downloading")
- Download.STATE_REMOVING -> Logger.log("Download Removing")
- Download.STATE_RESTARTING -> Logger.log("Download Restarting")
- }
- }
- }
- )
-
- downloadManager
- }
- }
-
- private var downloadDirectory: File? = null
-
- @Synchronized
- private fun getDownloadDirectory(context: Context): File {
- if (downloadDirectory == null) {
- downloadDirectory = context.getExternalFilesDir(null)
- if (downloadDirectory == null) {
- downloadDirectory = context.filesDir
- }
- }
- return downloadDirectory!!
- }
-
@OptIn(UnstableApi::class)
fun startAnimeDownloadService(
context: Context,
@@ -225,15 +91,6 @@ object Helper {
.setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ ->
- DownloadService.sendRemoveDownload(
- context,
- ExoplayerDownloadService::class.java,
- PrefManager.getAnimeDownloadPreferences().getString(
- animeDownloadTask.getTaskName(),
- ""
- ) ?: "",
- false
- )
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
@@ -243,12 +100,13 @@ object Helper {
episode,
MediaType.ANIME
)
- )
- AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
- if (!AnimeServiceDataSingleton.isServiceRunning) {
- val intent = Intent(context, AnimeDownloaderService::class.java)
- ContextCompat.startForegroundService(context, intent)
- AnimeServiceDataSingleton.isServiceRunning = true
+ ) {
+ AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
+ if (!AnimeServiceDataSingleton.isServiceRunning) {
+ val intent = Intent(context, AnimeDownloaderService::class.java)
+ ContextCompat.startForegroundService(context, intent)
+ AnimeServiceDataSingleton.isServiceRunning = true
+ }
}
}
.setNegativeButton("No") { _, _ -> }
@@ -263,18 +121,6 @@ object Helper {
}
}
- @OptIn(UnstableApi::class)
- fun getSimpleCache(context: Context): SimpleCache {
- return if (simpleCache == null) {
- val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
- val database = Injekt.get()
- 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(
diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
index a7b8858f..4f93cdde 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
@@ -13,6 +13,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
@@ -53,6 +54,7 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
+import ani.dantotsu.util.LauncherWrapper
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.CoroutineScope
@@ -66,7 +68,7 @@ import kotlin.math.abs
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
-
+ lateinit var launcher: LauncherWrapper
lateinit var binding: ActivityMediaBinding
private val scope = lifecycleScope
private val model: MediaDetailsViewModel by viewModels()
@@ -92,6 +94,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
onBackPressedDispatcher.onBackPressed()
return
}
+ val contract = ActivityResultContracts.OpenDocumentTree()
+ launcher = LauncherWrapper(this, contract)
+
mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null
@@ -576,4 +581,4 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
companion object {
var mediaSingleton: Media? = null
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
index 6672f37d..38affa0d 100644
--- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
+++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
@@ -5,6 +5,7 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.snackString
+import com.anggrayudi.storage.file.openOutputStream
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -51,21 +52,17 @@ class SubtitleDownloader {
downloadedType: DownloadedType
) {
try {
- val directory = DownloadsManager.getDirectory(
+ val directory = DownloadsManager.getSubDirectory(
context,
downloadedType.type,
+ false,
downloadedType.title,
downloadedType.chapter
- )
- if (!directory.exists()) { //just in case
- directory.mkdirs()
- }
+ ) ?: throw Exception("Could not create directory")
val type = loadSubtitleType(url)
- val subtiteFile = File(directory, "subtitle.${type}")
- if (subtiteFile.exists()) {
- subtiteFile.delete()
- }
- subtiteFile.createNewFile()
+ directory.findFile("subtitle.${type}")?.delete()
+ val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
+ ?: throw Exception("Could not create subtitle file")
val client = Injekt.get().client
val request = Request.Builder().url(url).build()
@@ -77,7 +74,8 @@ class SubtitleDownloader {
}
reponse.body.byteStream().use { input ->
- subtiteFile.outputStream().use { output ->
+ subtitleFile.openOutputStream(context, false).use { output ->
+ if (output == null) throw Exception("Could not open output stream")
input.copyTo(output)
}
}
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 8298318c..2554f283 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt
@@ -14,6 +14,7 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.annotation.OptIn
+import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
@@ -34,8 +35,8 @@ import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.download.anime.AnimeDownloaderService
-import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.dp
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -54,6 +55,8 @@ import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
+import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
+import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@@ -422,7 +425,19 @@ class AnimeWatchFragment : Fragment() {
}
fun onAnimeEpisodeDownloadClick(i: String) {
- model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
+ activity?.let{
+ if (!hasDirAccess(it)) {
+ (it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
+ if (success) {
+ model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
+ } else {
+ snackString("Permission is required to download")
+ }
+ }
+ } else {
+ model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
+ }
+ }
}
fun onAnimeEpisodeStopDownloadClick(i: String) {
@@ -442,8 +457,9 @@ class AnimeWatchFragment : Fragment() {
i,
MediaType.ANIME
)
- )
- episodeAdapter.purgeDownload(i)
+ ) {
+ episodeAdapter.purgeDownload(i)
+ }
}
@OptIn(UnstableApi::class)
@@ -454,20 +470,15 @@ class AnimeWatchFragment : Fragment() {
i,
MediaType.ANIME
)
- )
- val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
- val id = PrefManager.getAnimeDownloadPreferences().getString(
- taskName,
- ""
- ) ?: ""
- PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
- DownloadService.sendRemoveDownload(
- requireContext(),
- ExoplayerDownloadService::class.java,
- id,
- true
- )
- episodeAdapter.deleteDownload(i)
+ ) {
+ val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
+ val id = PrefManager.getAnimeDownloadPreferences().getString(
+ taskName,
+ ""
+ ) ?: ""
+ PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
+ episodeAdapter.deleteDownload(i)
+ }
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
@@ -531,7 +542,7 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
- if (download.title == media.mainName()) {
+ if (download.title == media.mainName().findValidName()) {
episodeAdapter.stopDownload(download.chapter)
}
}
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 68d3aabd..86e8af61 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt
@@ -10,7 +10,6 @@ import androidx.annotation.OptIn
import androidx.core.view.isVisible
import androidx.lifecycle.coroutineScope
import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.offline.DownloadIndex
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.updateProgress
@@ -18,10 +17,12 @@ import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding
+import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.getDirSize
import ani.dantotsu.download.anime.AnimeDownloaderService
-import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
+import ani.dantotsu.media.MediaType
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
import com.bumptech.glide.Glide
@@ -56,15 +57,7 @@ class EpisodeAdapter(
var arr: List = arrayListOf(),
var offlineMode: Boolean
) : RecyclerView.Adapter() {
-
- private lateinit var index: DownloadIndex
-
-
- init {
- if (offlineMode) {
- index = Helper.downloadManager(fragment.requireContext()).downloadIndex
- }
- }
+ val context = fragment.requireContext()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) {
@@ -248,17 +241,8 @@ class EpisodeAdapter(
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
- val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
- media.mainName(),
- episodeNumber
- )
- val id = PrefManager.getAnimeDownloadPreferences().getString(
- taskName,
- ""
- ) ?: ""
val size = try {
- val download = index.getDownload(id)
- bytesToHuman(download?.bytesDownloaded ?: 0)
+ bytesToHuman(getDirSize(context, MediaType.ANIME, media.mainName(), episodeNumber))
} catch (e: Exception) {
null
}
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 7daf8138..8211cb08 100644
--- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
+++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
@@ -104,7 +104,7 @@ import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding
import ani.dantotsu.defaultHeaders
-import ani.dantotsu.download.video.Helper
+import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.dp
import ani.dantotsu.getCurrentBrightnessValue
import ani.dantotsu.hideSystemBars
@@ -114,6 +114,7 @@ import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter
+import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.okHttpClient
import ani.dantotsu.others.AniSkip
@@ -394,7 +395,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
isCastApiAvailable = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
try {
- castContext = CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
+ castContext =
+ CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
castPlayer = CastPlayer(castContext!!)
castPlayer!!.setSessionAvailabilityListener(this)
} catch (e: Exception) {
@@ -442,41 +444,43 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}, AUDIO_CONTENT_TYPE_MOVIE, AUDIOFOCUS_GAIN)
if (System.getInt(contentResolver, System.ACCELEROMETER_ROTATION, 0) != 1) {
- if (PrefManager.getVal(PrefName.RotationPlayer)) {
- orientationListener =
- object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
- override fun onOrientationChanged(orientation: Int) {
- when (orientation) {
- in 45..135 -> {
- if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
- exoRotate.visibility = View.VISIBLE
+ if (PrefManager.getVal(PrefName.RotationPlayer)) {
+ orientationListener =
+ object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
+ override fun onOrientationChanged(orientation: Int) {
+ when (orientation) {
+ in 45..135 -> {
+ if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
+ exoRotate.visibility = View.VISIBLE
+ }
+ rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
+ }
+
+ in 225..315 -> {
+ if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+ exoRotate.visibility = View.VISIBLE
+ }
+ rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ }
+
+ in 315..360, in 0..45 -> {
+ if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
+ exoRotate.visibility = View.VISIBLE
+ }
+ rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ }
}
- rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
- }
- in 225..315 -> {
- if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
- exoRotate.visibility = View.VISIBLE
- }
- rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
- }
- in 315..360, in 0..45 -> {
- if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
- exoRotate.visibility = View.VISIBLE
- }
- rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
- }
+ orientationListener?.enable()
}
- orientationListener?.enable()
- }
- requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
- exoRotate.setOnClickListener {
- requestedOrientation = rotation
- it.visibility = View.GONE
- }
-}
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+ exoRotate.setOnClickListener {
+ requestedOrientation = rotation
+ it.visibility = View.GONE
+ }
+ }
setupSubFormatting(playerView)
@@ -1089,10 +1093,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
"nothing" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
)
+
"dantotsu" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("Watch on Dantotsu", getString(R.string.dantotsu))
)
+
"anilist" -> {
val userId = PrefManager.getVal(PrefName.AnilistUserId)
val anilistLink = "https://anilist.co/user/$userId/"
@@ -1101,6 +1107,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
RPC.Link("View My AniList", anilistLink)
)
}
+
else -> mutableListOf()
}
val presence = RPC.createPresence(
@@ -1113,7 +1120,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
- largeImage = media.cover?.let { RPC.Link(media.userPreferredName, it) },
+ largeImage = media.cover?.let {
+ RPC.Link(
+ media.userPreferredName,
+ it
+ )
+ },
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
buttons = buttons
)
@@ -1161,7 +1173,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (PrefManager.getVal(PrefName.Cast)) {
playerView.findViewById(R.id.exo_cast).apply {
visibility = View.VISIBLE
- if(PrefManager.getVal(PrefName.UseInternalCast)) {
+ if (PrefManager.getVal(PrefName.UseInternalCast)) {
try {
CastButtonFactory.setUpMediaRouteButton(context, this)
dialogFactory = CustomCastThemeFactory()
@@ -1324,7 +1336,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
)
@Suppress("UNCHECKED_CAST")
- val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf(), List::class.java) as List).toMutableList()
+ val list = (PrefManager.getNullableCustomVal(
+ "continueAnimeList",
+ listOf(),
+ List::class.java
+ ) as List).toMutableList()
if (list.contains(media.id)) list.remove(media.id)
list.add(media.id)
PrefManager.setCustomVal("continueAnimeList", list)
@@ -1418,7 +1434,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
cacheFactory = CacheDataSource.Factory().apply {
- setCache(Helper.getSimpleCache(this@ExoplayerView))
+ setCache(VideoCache.getInstance(this@ExoplayerView))
if (ext.server.offline) {
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
} else {
@@ -1435,15 +1451,28 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val downloadedMediaItem = if (ext.server.offline) {
val key = ext.server.name
- downloadId = PrefManager.getAnimeDownloadPreferences()
- .getString(key, null)
- if (downloadId != null) {
- Helper.downloadManager(this)
- .downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
+ val titleName = ext.server.name.split("/").first()
+ val episodeName = ext.server.name.split("/").last()
+
+ val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
+ if (directory != null) {
+ val files = directory.listFiles()
+ println(files)
+ val docFile = directory.listFiles().firstOrNull {
+ it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true
+ }
+ if (docFile != null) {
+ val uri = docFile.uri
+ MediaItem.Builder().setUri(uri).setMimeType(mimeType).build()
+ } else {
+ snackString("File not found")
+ null
+ }
} else {
- snackString("Download not found")
+ snackString("Directory not found")
null
}
+
} else null
mediaItem = if (downloadedMediaItem == null) {
@@ -1818,7 +1847,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (!functionstarted && !disappeared && PrefManager.getVal(PrefName.AutoHideTimeStamps)) {
disappearSkip()
- } else if (!PrefManager.getVal(PrefName.AutoHideTimeStamps)){
+ } else if (!PrefManager.getVal(PrefName.AutoHideTimeStamps)) {
skipTimeButton.visibility = View.VISIBLE
exoSkip.visibility = View.GONE
skipTimeText.text = new.skipType.getType()
@@ -2157,11 +2186,16 @@ class CustomCastButton : MediaRouteButton {
fun setCastCallback(castCallback: () -> Unit) {
this.castCallback = castCallback
}
+
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
- constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
+ context,
+ attrs,
+ defStyleAttr
+ )
override fun performClick(): Boolean {
return if (PrefManager.getVal(PrefName.UseInternalCast)) {
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 dccbd72c..26573b6e 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
@@ -16,6 +16,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@@ -34,6 +35,7 @@ import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.MangaServiceDataSingleton
import ani.dantotsu.dp
@@ -56,6 +58,8 @@ import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
+import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
+import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
@@ -190,7 +194,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
)
for (download in downloadManager.mangaDownloadedTypes) {
- if (download.title == media.mainName()) {
+ if (download.title == media.mainName().findValidName()) {
chapterAdapter.stopDownload(download.chapter)
}
}
@@ -434,51 +438,65 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
fun onMangaChapterDownloadClick(i: String) {
- if (!isNotificationPermissionGranted()) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- ActivityCompat.requestPermissions(
- requireActivity(),
- arrayOf(Manifest.permission.POST_NOTIFICATIONS),
- 1
- )
- }
- }
-
- model.continueMedia = false
- media.manga?.chapters?.get(i)?.let { chapter ->
- val parser =
- model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
- parser?.let {
- CoroutineScope(Dispatchers.IO).launch {
- val images = parser.imageList(chapter.sChapter)
-
- // Create a download task
- val downloadTask = MangaDownloaderService.DownloadTask(
- title = media.mainName(),
- chapter = chapter.title!!,
- imageData = images,
- sourceMedia = media,
- retries = 2,
- simultaneousDownloads = 2
+ activity?.let {
+ if (!isNotificationPermissionGranted()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ActivityCompat.requestPermissions(
+ it,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 1
)
+ }
+ }
+ fun continueDownload() {
+ model.continueMedia = false
+ media.manga?.chapters?.get(i)?.let { chapter ->
+ val parser =
+ model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
+ parser?.let {
+ CoroutineScope(Dispatchers.IO).launch {
+ val images = parser.imageList(chapter.sChapter)
- MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
+ // Create a download task
+ val downloadTask = MangaDownloaderService.DownloadTask(
+ title = media.mainName(),
+ chapter = chapter.title!!,
+ imageData = images,
+ sourceMedia = media,
+ retries = 2,
+ simultaneousDownloads = 2
+ )
- // If the service is not already running, start it
- if (!MangaServiceDataSingleton.isServiceRunning) {
- val intent = Intent(context, MangaDownloaderService::class.java)
- withContext(Dispatchers.Main) {
- ContextCompat.startForegroundService(requireContext(), intent)
+ MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
+
+ // If the service is not already running, start it
+ if (!MangaServiceDataSingleton.isServiceRunning) {
+ val intent = Intent(context, MangaDownloaderService::class.java)
+ withContext(Dispatchers.Main) {
+ ContextCompat.startForegroundService(requireContext(), intent)
+ }
+ MangaServiceDataSingleton.isServiceRunning = true
+ }
+
+ // Inform the adapter that the download has started
+ withContext(Dispatchers.Main) {
+ chapterAdapter.startDownload(i)
+ }
}
- MangaServiceDataSingleton.isServiceRunning = true
- }
-
- // Inform the adapter that the download has started
- withContext(Dispatchers.Main) {
- chapterAdapter.startDownload(i)
}
}
}
+ if (!hasDirAccess(it)) {
+ (it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
+ if (success) {
+ continueDownload()
+ } else {
+ snackString("Permission is required to download")
+ }
+ }
+ } else {
+ continueDownload()
+ }
}
}
@@ -500,8 +518,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
i,
MediaType.MANGA
)
- )
- chapterAdapter.deleteDownload(i)
+ ) {
+ chapterAdapter.deleteDownload(i)
+ }
}
fun onMangaChapterStopDownloadClick(i: String) {
@@ -518,8 +537,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
i,
MediaType.MANGA
)
- )
- chapterAdapter.purgeDownload(i)
+ ) {
+ chapterAdapter.purgeDownload(i)
+ }
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
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 4fde1ed8..7b91d6f7 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
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
+import android.net.Uri
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
@@ -176,6 +177,10 @@ abstract class BaseImageAdapter(
it.load(localFile.absoluteFile)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
+ } else if (link.url.startsWith("content://")) {
+ it.load(Uri.parse(link.url))
+ .skipMemoryCache(true)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
} else {
mangaCache.get(link.url)?.let { imageData ->
val bitmap = imageData.fetchAndProcessImage(
@@ -186,6 +191,7 @@ abstract class BaseImageAdapter(
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
}
+
}
}
?.let {
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 867912a7..c57f943f 100644
--- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
+++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
@@ -20,6 +20,7 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
+import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
@@ -94,23 +95,23 @@ class NovelReadFragment : Fragment(),
)
)
) {
- val file = File(
- context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "${DownloadsManager.novelLocation}/${media.mainName()}/${novel.name}/0.epub"
- )
- if (!file.exists()) return false
- val fileUri = FileProvider.getUriForFile(
- requireContext(),
- "${requireContext().packageName}.provider",
- file
- )
- val intent = Intent(context, NovelReaderActivity::class.java).apply {
- action = Intent.ACTION_VIEW
- setDataAndType(fileUri, "application/epub+zip")
- flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ try {
+ val directory =
+ DownloadsManager.getSubDirectory(context?:currContext()!!, MediaType.NOVEL, false, novel.name)
+ val file = directory?.findFile(novel.name)
+ if (file?.exists() == false) return false
+ val fileUri = file?.uri ?: return false
+ val intent = Intent(context, NovelReaderActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ setDataAndType(fileUri, "application/epub+zip")
+ flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ }
+ startActivity(intent)
+ return true
+ } catch (e: Exception) {
+ Logger.log(e)
+ return false
}
- startActivity(intent)
- return true
} else {
return false
}
@@ -135,7 +136,7 @@ class NovelReadFragment : Fragment(),
novel.name,
MediaType.NOVEL
)
- )
+ ) {}
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
diff --git a/app/src/main/java/ani/dantotsu/notifications/comment/CommentNotificationTask.kt b/app/src/main/java/ani/dantotsu/notifications/comment/CommentNotificationTask.kt
index b8cf6f7f..f6839558 100644
--- a/app/src/main/java/ani/dantotsu/notifications/comment/CommentNotificationTask.kt
+++ b/app/src/main/java/ani/dantotsu/notifications/comment/CommentNotificationTask.kt
@@ -46,11 +46,11 @@ class CommentNotificationTask : Task {
)
notifications =
- notifications?.filter { it.type != 3 || it.notificationId > recentGlobal }
+ notifications?.filter { !it.type.isGlobal() || it.notificationId > recentGlobal }
?.toMutableList()
val newRecentGlobal =
- notifications?.filter { it.type == 3 }?.maxOfOrNull { it.notificationId }
+ notifications?.filter { it.type.isGlobal() }?.maxOfOrNull { it.notificationId }
if (newRecentGlobal != null) {
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
}
@@ -313,4 +313,6 @@ class CommentNotificationTask : Task {
null
}
}
+
+ private fun Int?.isGlobal() = this == 3 || this == 420
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/others/Download.kt b/app/src/main/java/ani/dantotsu/others/Download.kt
index 3a266af4..b80c941d 100644
--- a/app/src/main/java/ani/dantotsu/others/Download.kt
+++ b/app/src/main/java/ani/dantotsu/others/Download.kt
@@ -36,16 +36,8 @@ object Download {
}
private fun getDownloadDir(context: Context): File {
- val direct: File
- if (PrefManager.getVal(PrefName.SdDl)) {
- val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
- val parentDirectory = arrayOfFiles[1].toString()
- direct = File(parentDirectory)
- if (!direct.exists()) direct.mkdirs()
- } else {
- direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
- if (!direct.exists()) direct.mkdirs()
- }
+ val direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
+ if (!direct.exists()) direct.mkdirs()
return direct
}
@@ -96,52 +88,10 @@ object Download {
when (PrefManager.getVal(PrefName.DownloadManager) as Int) {
1 -> oneDM(context, file, notif ?: fileName)
2 -> adm(context, file, fileName, folder)
- else -> defaultDownload(context, file, fileName, folder, notif ?: fileName)
+ else -> oneDM(context, file, notif ?: fileName)
}
}
- private fun defaultDownload(
- context: Context,
- file: FileUrl,
- fileName: String,
- folder: String,
- notif: String
- ) {
- val manager =
- context.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager
- val request: DownloadManager.Request = DownloadManager.Request(Uri.parse(file.url))
- file.headers.forEach {
- request.addRequestHeader(it.key, it.value)
- }
- CoroutineScope(Dispatchers.IO).launch {
- try {
- request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
-
- val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
- if (PrefManager.getVal(PrefName.SdDl) && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
- val parentDirectory = arrayOfFiles[1].toString() + folder
- val direct = File(parentDirectory)
- if (!direct.exists()) direct.mkdirs()
- request.setDestinationUri(Uri.fromFile(File("$parentDirectory$fileName")))
- } else {
- val direct = File(Environment.DIRECTORY_DOWNLOADS + "/Dantotsu$folder")
- if (!direct.exists()) direct.mkdirs()
- request.setDestinationInExternalPublicDir(
- Environment.DIRECTORY_DOWNLOADS,
- "/Dantotsu$folder$fileName"
- )
- }
- request.setTitle(notif)
- manager.enqueue(request)
- toast(currContext()?.getString(R.string.started_downloading, notif))
- } catch (e: SecurityException) {
- toast(currContext()?.getString(R.string.permission_required))
- } catch (e: Exception) {
- toast(e.toString())
- }
- }
- }
-
private fun oneDM(context: Context, file: FileUrl, notif: String) {
val appName =
if (isPackageInstalled("idm.internet.download.manager.plus", context.packageManager)) {
diff --git a/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt b/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt
index 346bd592..e721e84d 100644
--- a/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt
+++ b/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt
@@ -12,7 +12,6 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetImageBinding
-import ani.dantotsu.downloadsPermission
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmapOld
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
@@ -22,6 +21,7 @@ import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.shareImage
import ani.dantotsu.snackString
import ani.dantotsu.toast
+import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.ImageSource
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt
index 582419c9..f4e30c0d 100644
--- a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt
@@ -1,9 +1,12 @@
package ani.dantotsu.parsers
+import android.app.Application
import android.net.Uri
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
+import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.tryWithSuspend
@@ -18,6 +21,7 @@ import java.util.Locale
class OfflineAnimeParser : AnimeParser() {
private val downloadManager = Injekt.get()
+ private val context = Injekt.get()
override val name = "Offline"
override val saveName = "Offline"
@@ -29,22 +33,19 @@ class OfflineAnimeParser : AnimeParser() {
extra: Map?,
sAnime: SAnime
): List {
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "${DownloadsManager.animeLocation}/$animeLink"
- )
+ val directory = getSubDirectory(context, MediaType.ANIME, false, animeLink)
//get all of the folder names and add them to the list
val episodes = mutableListOf()
- if (directory.exists()) {
- directory.listFiles()?.forEach {
+ if (directory?.exists() == true) {
+ directory.listFiles().forEach {
//put the title and episdode number in the extra data
val extraData = mutableMapOf()
extraData["title"] = animeLink
- extraData["episode"] = it.name
+ extraData["episode"] = it.name!!
if (it.isDirectory) {
val episode = Episode(
- it.name,
- "$animeLink - ${it.name}",
+ it.name!!,
+ getTaskName(animeLink,it.name!!),
it.name,
null,
null,
@@ -131,18 +132,19 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
private fun getSubtitle(title: String, episode: String): List? {
currContext()?.let {
- DownloadsManager.getDirectory(
+ DownloadsManager.getSubDirectory(
it,
MediaType.ANIME,
+ false,
title,
episode
- ).listFiles()?.forEach { file ->
- if (file.name.contains("subtitle")) {
+ )?.listFiles()?.forEach { file ->
+ if (file.name?.contains("subtitle") == true) {
return listOf(
Subtitle(
"Downloaded Subtitle",
- Uri.fromFile(file).toString(),
- determineSubtitletype(file.absolutePath)
+ file.uri.toString(),
+ determineSubtitletype(file.name ?: "")
)
)
}
diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
index 983a53ec..a3a239a8 100644
--- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
@@ -1,9 +1,12 @@
package ani.dantotsu.parsers
+import android.app.Application
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.MediaNameAdapter
+import ani.dantotsu.media.MediaType
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
@@ -14,6 +17,7 @@ import java.io.File
class OfflineMangaParser : MangaParser() {
private val downloadManager = Injekt.get()
+ private val context = Injekt.get()
override val hostUrl: String = "Offline"
override val name: String = "Offline"
@@ -23,17 +27,14 @@ class OfflineMangaParser : MangaParser() {
extra: Map?,
sManga: SManga
): List {
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Manga/$mangaLink"
- )
+ val directory = getSubDirectory(context, MediaType.MANGA, false, mangaLink)
//get all of the folder names and add them to the list
val chapters = mutableListOf()
- if (directory.exists()) {
- directory.listFiles()?.forEach {
+ if (directory?.exists() == true) {
+ directory.listFiles().forEach {
if (it.isDirectory) {
val chapter = MangaChapter(
- it.name,
+ it.name!!,
"$mangaLink/${it.name}",
it.name,
null,
@@ -50,16 +51,15 @@ class OfflineMangaParser : MangaParser() {
}
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List {
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Manga/$chapterLink"
- )
+ val title = chapterLink.split("/").first()
+ val chapter = chapterLink.split("/").last()
+ val directory = getSubDirectory(context, MediaType.MANGA, false, title, chapter)
val images = mutableListOf()
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
- if (directory.exists()) {
- directory.listFiles()?.forEach {
+ if (directory?.exists() == true) {
+ directory.listFiles().forEach {
if (it.isFile) {
- val image = MangaImage(it.absolutePath, false, null)
+ val image = MangaImage(it.uri.toString(), false, null)
images.add(image)
}
}
diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt
index 11009aed..2ae88200 100644
--- a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt
@@ -1,9 +1,12 @@
package ani.dantotsu.parsers
+import android.app.Application
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
+import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.MediaNameAdapter
+import ani.dantotsu.media.MediaType
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -11,6 +14,7 @@ import java.io.File
class OfflineNovelParser : NovelParser() {
private val downloadManager = Injekt.get()
+ private val context = Injekt.get()
override val hostUrl: String = "Offline"
override val name: String = "Offline"
@@ -21,19 +25,16 @@ class OfflineNovelParser : NovelParser() {
override suspend fun loadBook(link: String, extra: Map?): Book {
//link should be a directory
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Novel/$link"
- )
+ val directory = getSubDirectory(context, MediaType.NOVEL, false, link)
val chapters = mutableListOf()
- if (directory.exists()) {
- directory.listFiles()?.forEach {
+ if (directory?.exists() == true) {
+ directory.listFiles().forEach {
if (it.isDirectory) {
val chapter = Book(
- it.name,
- it.absolutePath + "/cover.jpg",
+ it.name?:"Unknown",
+ it.uri.toString(),
null,
- listOf(it.absolutePath + "/0.epub")
+ listOf(it.uri.toString())
)
chapters.add(chapter)
}
@@ -60,20 +61,16 @@ class OfflineNovelParser : NovelParser() {
val returnList: MutableList = mutableListOf()
for (title in returnTitles) {
//need to search the subdirectories for the ShowResponses
- val directory = File(
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
- "Dantotsu/Novel/$title"
- )
+ val directory = getSubDirectory(context, MediaType.NOVEL, false, title)
val names = mutableListOf()
- if (directory.exists()) {
- directory.listFiles()?.forEach {
+ if (directory?.exists() == true) {
+ directory.listFiles().forEach {
if (it.isDirectory) {
- names.add(it.name)
+ names.add(it.name?: "Unknown")
}
}
}
- val cover =
- currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
+ val cover = directory?.findFile("cover.jpg")?.uri.toString()
names.forEach {
returnList.add(ShowResponse(it, it, cover))
}
diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt
index de9e9841..6f2d7a08 100644
--- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt
@@ -5,6 +5,7 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Animatable
+import android.net.Uri
import android.os.Build
import android.os.Build.BRAND
import android.os.Build.DEVICE
@@ -52,8 +53,6 @@ import ani.dantotsu.databinding.ActivitySettingsMangaBinding
import ani.dantotsu.databinding.ActivitySettingsNotificationsBinding
import ani.dantotsu.databinding.ActivitySettingsThemeBinding
import ani.dantotsu.download.DownloadsManager
-import ani.dantotsu.download.video.ExoplayerDownloadService
-import ani.dantotsu.downloadsPermission
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaType
@@ -82,7 +81,10 @@ import ani.dantotsu.startMainActivity
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
+import ani.dantotsu.util.LauncherWrapper
import ani.dantotsu.util.Logger
+import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
+import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.textfield.TextInputEditText
import eltos.simpledialogfragment.SimpleDialog
@@ -91,7 +93,9 @@ import eltos.simpledialogfragment.color.SimpleColorDialog
import eu.kanade.domain.base.BasePreferences
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
@@ -104,6 +108,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
}
lateinit var binding: ActivitySettingsBinding
+ lateinit var launcher: LauncherWrapper
private lateinit var bindingAccounts: ActivitySettingsAccountsBinding
private lateinit var bindingTheme: ActivitySettingsThemeBinding
private lateinit var bindingExtensions: ActivitySettingsExtensionsBinding
@@ -115,6 +120,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
private val extensionInstaller = Injekt.get().extensionInstaller()
private var cursedCounter = 0
+ @kotlin.OptIn(DelicateCoroutinesApi::class)
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -166,6 +172,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
}
}
}
+ val contract = ActivityResultContracts.OpenDocumentTree()
+ launcher = LauncherWrapper(this, contract)
binding.settingsVersion.text = getString(R.string.version_current, BuildConfig.VERSION_NAME)
binding.settingsVersion.setOnLongClickListener {
@@ -457,11 +465,6 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
.setPositiveButton(R.string.yes) { dialog, _ ->
val downloadsManager = Injekt.get()
downloadsManager.purgeDownloads(MediaType.ANIME)
- DownloadService.sendRemoveAllDownloads(
- this@SettingsActivity,
- ExoplayerDownloadService::class.java,
- false
- )
dialog.dismiss()
}
.setNegativeButton(R.string.no) { dialog, _ ->
@@ -724,20 +727,6 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
restartApp(binding.root)
}
- settingsDownloadInSd.isChecked = PrefManager.getVal(PrefName.SdDl)
- settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- val arrayOfFiles = ContextCompat.getExternalFilesDirs(this@SettingsActivity, null)
- if (arrayOfFiles.size > 1 && arrayOfFiles[1] != null) {
- PrefManager.setVal(PrefName.SdDl, true)
- } else {
- settingsDownloadInSd.isChecked = false
- PrefManager.setVal(PrefName.SdDl, true)
- snackString(getString(R.string.noSdFound))
- }
- } else PrefManager.setVal(PrefName.SdDl, true)
- }
-
settingsContinueMedia.isChecked = PrefManager.getVal(PrefName.ContinueMedia)
settingsContinueMedia.setOnCheckedChangeListener { _, isChecked ->
PrefManager.setVal(PrefName.ContinueMedia, isChecked)
@@ -757,6 +746,44 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
PrefManager.setVal(PrefName.AdultOnly, isChecked)
restartApp(binding.root)
}
+
+ settingsDownloadLocation.setOnClickListener {
+ val dialog = AlertDialog.Builder(this@SettingsActivity, R.style.MyPopup)
+ .setTitle(R.string.change_download_location)
+ .setMessage(R.string.download_location_msg)
+ .setPositiveButton(R.string.ok) { dialog, _ ->
+ val oldUri = PrefManager.getVal(PrefName.DownloadsDir)
+ launcher.registerForCallback { success ->
+ if (success) {
+ toast(getString(R.string.please_wait))
+ val newUri = PrefManager.getVal(PrefName.DownloadsDir)
+ GlobalScope.launch(Dispatchers.IO) {
+ Injekt.get().moveDownloadsDir(
+ this@SettingsActivity,
+ Uri.parse(oldUri), Uri.parse(newUri)
+ ) { finished, message ->
+ if (finished) {
+ toast(getString(R.string.success))
+ } else {
+ toast(message)
+ }
+ }
+ }
+ } else {
+ toast(getString(R.string.error))
+ }
+ }
+ launcher.launch()
+ dialog.dismiss()
+ }
+ .setNeutralButton(R.string.cancel) { dialog, _ ->
+ dialog.dismiss()
+ }
+ .create()
+ dialog.window?.setDimAmount(0.8f)
+ dialog.show()
+ }
+
var previousStart: View = when (PrefManager.getVal(PrefName.DefaultStartUpTab)) {
0 -> uiSettingsAnime
1 -> uiSettingsHome
diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt
index aebc894a..414a45cb 100644
--- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt
+++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt
@@ -13,7 +13,6 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
OfflineView(Pref(Location.General, Int::class, 0)),
DownloadManager(Pref(Location.General, Int::class, 0)),
NSFWExtension(Pref(Location.General, Boolean::class, true)),
- SdDl(Pref(Location.General, Boolean::class, false)),
ContinueMedia(Pref(Location.General, Boolean::class, true)),
SearchSources(Pref(Location.General, Boolean::class, true)),
RecentlyListOnly(Pref(Location.General, Boolean::class, false)),
@@ -182,6 +181,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
RecentGlobalNotification(Pref(Location.Irrelevant, Int::class, 0)),
CommentNotificationStore(Pref(Location.Irrelevant, List::class, listOf())),
UnreadCommentNotifications(Pref(Location.Irrelevant, Int::class, 0)),
+ DownloadsDir(Pref(Location.Irrelevant, String::class, "")),
//Protected
DiscordToken(Pref(Location.Protected, String::class, "")),
diff --git a/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt b/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt
new file mode 100644
index 00000000..485b0dd9
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt
@@ -0,0 +1,132 @@
+package ani.dantotsu.util
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import ani.dantotsu.R
+import ani.dantotsu.settings.saving.PrefManager
+import ani.dantotsu.settings.saving.PrefName
+import ani.dantotsu.toast
+
+class StoragePermissions {
+ companion object {
+ fun downloadsPermission(activity: AppCompatActivity): Boolean {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
+ val permissions = arrayOf(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+
+ val requiredPermissions = permissions.filter {
+ ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
+ }.toTypedArray()
+
+ return if (requiredPermissions.isNotEmpty()) {
+ ActivityCompat.requestPermissions(
+ activity,
+ requiredPermissions,
+ DOWNLOADS_PERMISSION_REQUEST_CODE
+ )
+ false
+ } else {
+ true
+ }
+ }
+
+ fun hasDirAccess(context: Context, path: String): Boolean {
+ val uri = pathToUri(path)
+ return context.contentResolver.persistedUriPermissions.any {
+ it.uri == uri && it.isReadPermission && it.isWritePermission
+ }
+
+ }
+
+ fun hasDirAccess(context: Context, uri: Uri): Boolean {
+ return context.contentResolver.persistedUriPermissions.any {
+ it.uri == uri && it.isReadPermission && it.isWritePermission
+ }
+ }
+
+ fun hasDirAccess(context: Context): Boolean {
+ val path = PrefManager.getVal(PrefName.DownloadsDir)
+ return hasDirAccess(context, path)
+ }
+
+ fun AppCompatActivity.accessAlertDialog(launcher: LauncherWrapper,
+ force: Boolean = false,
+ complete: (Boolean) -> Unit
+ ) {
+ if ((PrefManager.getVal(PrefName.DownloadsDir).isNotEmpty() || hasDirAccess(this)) && !force) {
+ complete(true)
+ return
+ }
+ val builder = AlertDialog.Builder(this, R.style.MyPopup)
+ builder.setTitle(getString(R.string.dir_access))
+ builder.setMessage(getString(R.string.dir_access_msg))
+ builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
+ launcher.registerForCallback(complete)
+ launcher.launch()
+ dialog.dismiss()
+ }
+ builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
+ dialog.dismiss()
+ complete(false)
+ }
+ val dialog = builder.show()
+ dialog.window?.setDimAmount(0.8f)
+ }
+
+ private fun pathToUri(path: String): Uri {
+ return Uri.parse(path)
+ }
+
+ private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
+ }
+}
+
+
+class LauncherWrapper(
+ activity: AppCompatActivity,
+ contract: ActivityResultContracts.OpenDocumentTree)
+{
+ private var launcher: ActivityResultLauncher
+ var complete: (Boolean) -> Unit = {}
+ init{
+ launcher = activity.registerForActivityResult(contract) { uri ->
+ if (uri != null) {
+ activity.contentResolver.takePersistableUriPermission(
+ uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ )
+
+ if (StoragePermissions.hasDirAccess(activity, uri)) {
+ PrefManager.setVal(PrefName.DownloadsDir, uri.toString())
+ complete(true)
+ } else {
+ toast(activity.getString(R.string.dir_error))
+ complete(false)
+ }
+ } else {
+ toast(activity.getString(R.string.dir_error))
+ complete(false)
+ }
+ }
+ }
+
+ fun registerForCallback(callback: (Boolean) -> Unit) {
+ complete = callback
+ }
+
+ fun launch() {
+ launcher.launch(null)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt
index 197be008..51f593a9 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import okhttp3.Headers
@@ -69,7 +70,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
- add("User-Agent", network.defaultUserAgentProvider())
+ add("User-Agent", defaultUserAgentProvider())
}
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
index 8ae4a8fd..f7c08d19 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
@@ -89,5 +89,7 @@ class NetworkHelper(
responseParser = Mapper
)
- fun defaultUserAgentProvider() = PrefManager.getVal(PrefName.DefaultUserAgent)
+ companion object {
+ fun defaultUserAgentProvider() = PrefManager.getVal(PrefName.DefaultUserAgent)
+ }
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt
index 16fd9935..613b58ba 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
@@ -69,7 +70,7 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
- add("User-Agent", network.defaultUserAgentProvider())
+ add("User-Agent", defaultUserAgentProvider())
}
/**
diff --git a/app/src/main/res/layout/activity_settings_common.xml b/app/src/main/res/layout/activity_settings_common.xml
index 0171808d..9916e78a 100644
--- a/app/src/main/res/layout/activity_settings_common.xml
+++ b/app/src/main/res/layout/activity_settings_common.xml
@@ -191,27 +191,6 @@
android:layout_marginBottom="16dp"
android:background="?android:attr/listDivider" />
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index db44578f..d92b3512 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -748,7 +748,9 @@
Follows you
Mutual
Success
-
+ Some error occurred
+ Error: %1$s
+ Please wait
Upcoming
No shows to display
Extension Name
@@ -866,6 +868,11 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
Trending Manhwa
Liked By
Adult only content
+ Your path could not be set
+ Downloads access
+ Please choose a directory to save your downloads
+ Change Download Location
+ Are you sure you want to change the download location?\nOld downloads may no longer be accessible.
Report
Ban