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
This commit is contained in:
parent
75e90541c9
commit
720b40afa7
35 changed files with 1162 additions and 1018 deletions
|
@ -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'
|
||||
|
|
|
@ -370,16 +370,6 @@
|
|||
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name=".download.video.ExoplayerDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
||||
android:exported="false"
|
||||
|
|
|
@ -620,9 +620,14 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
|||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||
if (file?.url?.isNotEmpty() == true) {
|
||||
tryWith {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
if (file.url.startsWith("content://")) {
|
||||
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
|
||||
.override(size).into(this)
|
||||
} else {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<NetworkHelper>().defaultUserAgentProvider()
|
||||
defaultUserAgentProvider()
|
||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||
)
|
||||
|
||||
|
|
|
@ -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<DownloadedType>) //for debugging
|
||||
{
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
val file = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/downloads.json"
|
||||
)
|
||||
if (file.parentFile?.exists() == false) {
|
||||
file.parentFile?.mkdirs()
|
||||
fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) {
|
||||
try {
|
||||
if (oldUri == newUri) {
|
||||
finished(false, "Source and destination are the same")
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
val oldBase =
|
||||
DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
|
||||
val newBase =
|
||||
DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
|
||||
val folder =
|
||||
oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
|
||||
folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object:
|
||||
FolderCallback() {
|
||||
override fun onFailed(errorCode: ErrorCode) {
|
||||
when (errorCode) {
|
||||
ErrorCode.CANCELED -> finished(false, "Move canceled")
|
||||
ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(false, "Cannot create file in target")
|
||||
ErrorCode.INVALID_TARGET_FOLDER -> finished(true, "Invalid target folder") // seems to still work
|
||||
ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(false, "No space left on target path")
|
||||
ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
|
||||
ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(false, "Source folder not found")
|
||||
ErrorCode.STORAGE_PERMISSION_DENIED -> finished(false, "Storage permission denied")
|
||||
ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(false, "Target folder cannot have same path with source folder")
|
||||
else -> finished(false, "Failed to move downloads: $errorCode")
|
||||
}
|
||||
Logger.log("Failed to move downloads: $errorCode")
|
||||
super.onFailed(errorCode)
|
||||
}
|
||||
|
||||
override fun onCompleted(result: Result) {
|
||||
finished(true, "Successfully moved downloads")
|
||||
super.onCompleted(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
snackString("Error: ${e.message}")
|
||||
finished(false, "Failed to move downloads: ${e.message}")
|
||||
return
|
||||
}
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
|
||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||
|
@ -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<String>(PrefName.DownloadsDir))
|
||||
if (baseDirectory == Uri.EMPTY) return null
|
||||
var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
|
||||
base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
|
||||
return when (type) {
|
||||
MediaType.MANGA -> {
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${download.failureReason}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
break
|
||||
}
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
|
||||
Logger.log("Download completed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download completed")
|
||||
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||
task.getTaskName(),
|
||||
task.video.file.url
|
||||
).apply()
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
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<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${getTaskName(task.title, task.episode)}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
break
|
||||
}
|
||||
builder.setProgress(
|
||||
100, percent.coerceAtMost(99),
|
||||
false
|
||||
)
|
||||
broadcastDownloadProgress(
|
||||
task.episode,
|
||||
percent.coerceAtMost(99)
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
if (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<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${getTaskName(task.title, task.episode)}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
return@withContext
|
||||
}
|
||||
Logger.log("Download completed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
getTaskName(
|
||||
task.title,
|
||||
task.episode
|
||||
)
|
||||
} Download completed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${getTaskName(task.title, task.episode)} Download completed")
|
||||
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||
task.getTaskName(),
|
||||
task.video.file.url
|
||||
).apply()
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFinished(task.episode)
|
||||
} else throw Exception("Download failed")
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
||||
|
@ -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<SChapter> {
|
||||
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<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
|
|
|
@ -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<ImageView>(R.id.downloadedList)
|
||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
val layoutCompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
var selected = when (style) {
|
||||
0 -> layoutList
|
||||
1 -> layoutcompact
|
||||
1 -> layoutCompact
|
||||
else -> layoutList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
|
@ -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<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
if (downloadsJob.isActive) {
|
||||
downloadsJob.cancel()
|
||||
}
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
}
|
||||
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<SChapter> {
|
||||
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
|
||||
|
|
|
@ -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<Deferred<Bitmap?>>()
|
||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
getSubDirectory(
|
||||
this@MangaDownloaderService,
|
||||
MediaType.MANGA,
|
||||
false,
|
||||
task.title,
|
||||
task.chapter
|
||||
)?.deleteRecursively(this@MangaDownloaderService)
|
||||
|
||||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
|
@ -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<SChapter> {
|
||||
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) {
|
||||
|
|
|
@ -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<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
if (downloadsJob.isActive) {
|
||||
downloadsJob.cancel()
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
downloads = listOf()
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
}
|
||||
downloads += 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<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return 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,
|
||||
|
|
|
@ -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<SChapter> {
|
||||
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) {
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
package ani.dantotsu.download.video
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.PlatformScheduler
|
||||
import androidx.media3.exoplayer.scheduler.Scheduler
|
||||
import ani.dantotsu.R
|
||||
|
||||
@UnstableApi
|
||||
class ExoplayerDownloadService :
|
||||
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||
companion object {
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||
}
|
||||
|
||||
override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
|
||||
|
||||
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
||||
|
||||
override fun getForegroundNotification(
|
||||
downloads: MutableList<Download>,
|
||||
notMetRequirements: Int
|
||||
): Notification =
|
||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||
this,
|
||||
R.drawable.mono,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
notMetRequirements
|
||||
)
|
||||
}
|
|
@ -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<StandaloneDatabaseProvider>()
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val okHttpClient = networkHelper.client
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
dataSource
|
||||
}
|
||||
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||
val downloadManager = DownloadManager(
|
||||
context,
|
||||
database,
|
||||
getSimpleCache(context),
|
||||
dataSourceFactory,
|
||||
executorService
|
||||
).apply {
|
||||
requirements =
|
||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
maxParallelDownloads = 3
|
||||
}
|
||||
downloadManager.addListener( //for testing
|
||||
object : DownloadManager.Listener {
|
||||
override fun onDownloadChanged(
|
||||
downloadManager: DownloadManager,
|
||||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
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<StandaloneDatabaseProvider>()
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<NetworkHelper>().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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Episode> = arrayListOf(),
|
||||
var offlineMode: Boolean
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private lateinit var index: DownloadIndex
|
||||
|
||||
|
||||
init {
|
||||
if (offlineMode) {
|
||||
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
|
||||
}
|
||||
}
|
||||
val context = fragment.requireContext()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return (when (viewType) {
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<String>(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<CustomCastButton>(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<Int>(), List::class.java) as List<Int>).toMutableList()
|
||||
val list = (PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
List::class.java
|
||||
) as List<Int>).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<Boolean>(PrefName.AutoHideTimeStamps)){
|
||||
} else if (!PrefManager.getVal<Boolean>(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)) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<DownloadsManager>()
|
||||
private val context = Injekt.get<Application>()
|
||||
|
||||
override val name = "Offline"
|
||||
override val saveName = "Offline"
|
||||
|
@ -29,22 +33,19 @@ class OfflineAnimeParser : AnimeParser() {
|
|||
extra: Map<String, String>?,
|
||||
sAnime: SAnime
|
||||
): List<Episode> {
|
||||
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<Episode>()
|
||||
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<String, String>()
|
||||
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<Subtitle>? {
|
||||
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 ?: "")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<DownloadsManager>()
|
||||
private val context = Injekt.get<Application>()
|
||||
|
||||
override val hostUrl: String = "Offline"
|
||||
override val name: String = "Offline"
|
||||
|
@ -23,17 +27,14 @@ class OfflineMangaParser : MangaParser() {
|
|||
extra: Map<String, String>?,
|
||||
sManga: SManga
|
||||
): List<MangaChapter> {
|
||||
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<MangaChapter>()
|
||||
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<MangaImage> {
|
||||
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<MangaImage>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DownloadsManager>()
|
||||
private val context = Injekt.get<Application>()
|
||||
|
||||
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<String, String>?): 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<Book>()
|
||||
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<ShowResponse> = 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<String>()
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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<BasePreferences>().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>()
|
||||
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<String>(PrefName.DownloadsDir)
|
||||
launcher.registerForCallback { success ->
|
||||
if (success) {
|
||||
toast(getString(R.string.please_wait))
|
||||
val newUri = PrefManager.getVal<String>(PrefName.DownloadsDir)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
Injekt.get<DownloadsManager>().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<Int>(PrefName.DefaultStartUpTab)) {
|
||||
0 -> uiSettingsAnime
|
||||
1 -> uiSettingsHome
|
||||
|
|
|
@ -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<CommentStore>())),
|
||||
UnreadCommentNotifications(Pref(Location.Irrelevant, Int::class, 0)),
|
||||
DownloadsDir(Pref(Location.Irrelevant, String::class, "")),
|
||||
|
||||
//Protected
|
||||
DiscordToken(Pref(Location.Protected, String::class, "")),
|
||||
|
|
132
app/src/main/java/ani/dantotsu/util/StoragePermissions.kt
Normal file
132
app/src/main/java/ani/dantotsu/util/StoragePermissions.kt
Normal file
|
@ -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<String>(PrefName.DownloadsDir)
|
||||
return hasDirAccess(context, path)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.accessAlertDialog(launcher: LauncherWrapper,
|
||||
force: Boolean = false,
|
||||
complete: (Boolean) -> Unit
|
||||
) {
|
||||
if ((PrefManager.getVal<String>(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<Uri?>
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -89,5 +89,7 @@ class NetworkHelper(
|
|||
responseParser = Mapper
|
||||
)
|
||||
|
||||
fun defaultUserAgentProvider() = PrefManager.getVal<String>(PrefName.DefaultUserAgent)
|
||||
companion object {
|
||||
fun defaultUserAgentProvider() = PrefManager.getVal<String>(PrefName.DefaultUserAgent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -191,27 +191,6 @@
|
|||
android:layout_marginBottom="16dp"
|
||||
android:background="?android:attr/listDivider" />
|
||||
|
||||
<!--TODO: Add support for SD card-->
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/settingsDownloadInSd"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false"
|
||||
android:drawableStart="@drawable/ic_round_sd_card_24"
|
||||
android:drawablePadding="16dp"
|
||||
android:elegantTextHeight="true"
|
||||
android:enabled="false"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:minHeight="64dp"
|
||||
android:text="@string/downloadInSd"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnBackground"
|
||||
android:visibility="gone"
|
||||
app:cornerRadius="0dp"
|
||||
app:drawableTint="?attr/colorPrimary"
|
||||
app:showText="false"
|
||||
app:thumbTint="@color/button_switch_track" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -317,5 +296,29 @@
|
|||
app:drawableTint="?attr/colorPrimary"
|
||||
app:showText="false"
|
||||
app:thumbTint="@color/button_switch_track" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/settingsDownloadLocation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="-31dp"
|
||||
android:layout_marginEnd="-31dp"
|
||||
android:background="@drawable/ui_bg"
|
||||
android:backgroundTint="?attr/colorSecondary"
|
||||
android:backgroundTintMode="src_atop"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:paddingStart="31dp"
|
||||
android:paddingEnd="31dp"
|
||||
android:text="@string/change_download_location"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="?attr/colorOnBackground"
|
||||
app:cornerRadius="0dp"
|
||||
app:icon="@drawable/ic_round_source_24"
|
||||
app:iconPadding="16dp"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="?attr/colorPrimary" />
|
||||
</ani.dantotsu.others.Xpandable>
|
||||
</merge>
|
|
@ -748,7 +748,9 @@
|
|||
<string name="follows_you">Follows you</string>
|
||||
<string name="mutual">Mutual</string>
|
||||
<string name="success">Success</string>
|
||||
|
||||
<string name="error">Some error occurred</string>
|
||||
<string name="error_msg">Error: %1$s</string>
|
||||
<string name="please_wait">Please wait</string>
|
||||
<string name="upcoming">Upcoming</string>
|
||||
<string name="no_shows_to_display">No shows to display</string>
|
||||
<string name="extension_name">Extension Name</string>
|
||||
|
@ -866,6 +868,11 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
|
|||
<string name="trending_manhwa">Trending Manhwa</string>
|
||||
<string name="liked_by">Liked By</string>
|
||||
<string name="adult_only_content">Adult only content</string>
|
||||
<string name="dir_error">Your path could not be set</string>
|
||||
<string name="dir_access">Downloads access</string>
|
||||
<string name="dir_access_msg">Please choose a directory to save your downloads</string>
|
||||
<string name="change_download_location">Change Download Location</string>
|
||||
<string name="download_location_msg">Are you sure you want to change the download location?\nOld downloads may no longer be accessible.</string>
|
||||
<string name="report">Report</string>
|
||||
<string name="ban">Ban</string>
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue