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"
|
versionName "3.0.0"
|
||||||
versionCode 300000000
|
versionCode 300000000
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
|
universalApk true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions += "store"
|
flavorDimensions += "store"
|
||||||
|
@ -99,6 +107,7 @@ dependencies {
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.webkit:webkit:1.10.0'
|
implementation 'androidx.webkit:webkit:1.10.0'
|
||||||
|
implementation "com.anggrayudi:storage:1.5.5"
|
||||||
|
|
||||||
// Glide
|
// Glide
|
||||||
ext.glide_version = '4.16.0'
|
ext.glide_version = '4.16.0'
|
||||||
|
@ -149,6 +158,9 @@ dependencies {
|
||||||
// String Matching
|
// String Matching
|
||||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
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
|
// Aniyomi
|
||||||
implementation 'io.reactivex:rxjava:1.3.8'
|
implementation 'io.reactivex:rxjava:1.3.8'
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
|
|
|
@ -370,16 +370,6 @@
|
||||||
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
|
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
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
|
<service
|
||||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|
|
@ -620,11 +620,16 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||||
if (file?.url?.isNotEmpty() == true) {
|
if (file?.url?.isNotEmpty() == true) {
|
||||||
tryWith {
|
tryWith {
|
||||||
|
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 }
|
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
|
||||||
.into(this)
|
.into(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
||||||
|
@ -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) {
|
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||||
|
|
||||||
val contentUri = FileProvider.getUriForFile(
|
val contentUri = FileProvider.getUriForFile(
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ani.dantotsu
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
@ -15,12 +16,11 @@ import android.os.Looper
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.OnClickListener
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnticipateInterpolator
|
import android.view.animation.AnticipateInterpolator
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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 index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||||
val downloadCursor = index.getDownloads()
|
val downloadCursor = index.getDownloads()
|
||||||
while (downloadCursor.moveToNext()) {
|
while (downloadCursor.moveToNext()) {
|
||||||
|
@ -458,7 +458,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/ //TODO: remove this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestart() {
|
override fun onRestart() {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.ResponseParser
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
import com.lagradost.nicehttp.addGenericDns
|
import com.lagradost.nicehttp.addGenericDns
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -40,7 +41,7 @@ fun initializeNetwork() {
|
||||||
|
|
||||||
defaultHeaders = mapOf(
|
defaultHeaders = mapOf(
|
||||||
"User-Agent" to
|
"User-Agent" to
|
||||||
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
defaultUserAgentProvider()
|
||||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
package ani.dantotsu.download
|
package ani.dantotsu.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||||
import ani.dantotsu.media.MediaType
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
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.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
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
|
import java.io.Serializable
|
||||||
|
|
||||||
class DownloadsManager(private val context: Context) {
|
class DownloadsManager(private val context: Context) {
|
||||||
|
@ -42,27 +53,29 @@ class DownloadsManager(private val context: Context) {
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeDownload(downloadedType: DownloadedType) {
|
fun removeDownload(downloadedType: DownloadedType, onFinished: () -> Unit) {
|
||||||
downloadsList.remove(downloadedType)
|
downloadsList.remove(downloadedType)
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
removeDirectory(downloadedType)
|
removeDirectory(downloadedType)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMedia(title: String, type: MediaType) {
|
fun removeMedia(title: String, type: MediaType) {
|
||||||
val subDirectory = type.asText()
|
val baseDirectory = getBaseDirectory(context, type)
|
||||||
val directory = File(
|
val directory = baseDirectory?.findFolder(title)
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
if (directory?.exists() == true) {
|
||||||
"Dantotsu/$subDirectory/$title"
|
val deleted = directory.deleteRecursively(context, false)
|
||||||
)
|
|
||||||
if (directory.exists()) {
|
|
||||||
val deleted = directory.deleteRecursively()
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
snackString("Successfully deleted")
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
snackString("Failed to delete directory")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
snackString("Directory does not exist")
|
||||||
cleanDownloads()
|
cleanDownloads()
|
||||||
}
|
}
|
||||||
when (type) {
|
when (type) {
|
||||||
|
@ -89,23 +102,17 @@ class DownloadsManager(private val context: Context) {
|
||||||
|
|
||||||
private fun cleanDownload(type: MediaType) {
|
private fun cleanDownload(type: MediaType) {
|
||||||
// remove all folders that are not in the downloads list
|
// remove all folders that are not in the downloads list
|
||||||
val subDirectory = type.asText()
|
val directory = getBaseDirectory(context, type)
|
||||||
val directory = File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/$subDirectory"
|
|
||||||
)
|
|
||||||
val downloadsSubLists = when (type) {
|
val downloadsSubLists = when (type) {
|
||||||
MediaType.MANGA -> mangaDownloadedTypes
|
MediaType.MANGA -> mangaDownloadedTypes
|
||||||
MediaType.ANIME -> animeDownloadedTypes
|
MediaType.ANIME -> animeDownloadedTypes
|
||||||
else -> novelDownloadedTypes
|
else -> novelDownloadedTypes
|
||||||
}
|
}
|
||||||
if (directory.exists()) {
|
if (directory?.exists() == true && directory.isDirectory) {
|
||||||
val files = directory.listFiles()
|
val files = directory.listFiles()
|
||||||
if (files != null) {
|
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||||
file.deleteRecursively()
|
file.deleteRecursively(context, false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,27 +120,57 @@ class DownloadsManager(private val context: Context) {
|
||||||
val iterator = downloadsList.iterator()
|
val iterator = downloadsList.iterator()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val download = iterator.next()
|
val download = iterator.next()
|
||||||
val downloadDir = File(directory, download.title)
|
val downloadDir = directory?.findFolder(download.title)
|
||||||
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) {
|
||||||
{
|
try {
|
||||||
val jsonString = gson.toJson(downloadsList)
|
if (oldUri == newUri) {
|
||||||
val file = File(
|
finished(false, "Source and destination are the same")
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
return
|
||||||
"Dantotsu/downloads.json"
|
|
||||||
)
|
|
||||||
if (file.parentFile?.exists() == false) {
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
}
|
}
|
||||||
if (!file.exists()) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
file.createNewFile()
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
file.writeText(jsonString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||||
|
@ -149,98 +186,35 @@ class DownloadsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||||
val directory = when (downloadedType.type) {
|
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
||||||
MediaType.MANGA -> {
|
val directory =
|
||||||
File(
|
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
|
||||||
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}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the directory exists and delete it recursively
|
// Check if the directory exists and delete it recursively
|
||||||
if (directory.exists()) {
|
if (directory?.exists() == true) {
|
||||||
val deleted = directory.deleteRecursively()
|
val deleted = directory.deleteRecursively(context, false)
|
||||||
if (deleted) {
|
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()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
snackString("Failed to delete directory")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
snackString("Directory does not exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun purgeDownloads(type: MediaType) {
|
fun purgeDownloads(type: MediaType) {
|
||||||
val directory = when (type) {
|
val directory = getBaseDirectory(context, type)
|
||||||
MediaType.MANGA -> {
|
if (directory?.exists() == true) {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
val deleted = directory.deleteRecursively(context, false)
|
||||||
}
|
|
||||||
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()
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
snackString("Successfully deleted")
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
snackString("Failed to delete directory")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
snackString("Directory does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadsList.removeAll { it.type == type }
|
downloadsList.removeAll { it.type == type }
|
||||||
|
@ -248,59 +222,95 @@ class DownloadsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val novelLocation = "Dantotsu/Novel"
|
private const val BASE_LOCATION = "Dantotsu"
|
||||||
const val mangaLocation = "Dantotsu/Manga"
|
private const val MANGA_SUB_LOCATION = "Manga"
|
||||||
const val animeLocation = "Dantotsu/Anime"
|
private const val ANIME_SUB_LOCATION = "Anime"
|
||||||
|
private const val NOVEL_SUB_LOCATION = "Novel"
|
||||||
|
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
|
||||||
|
|
||||||
fun getDirectory(
|
fun String?.findValidName(): String {
|
||||||
context: Context,
|
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||||
type: MediaType,
|
}
|
||||||
title: String,
|
|
||||||
chapter: String? = null
|
/**
|
||||||
): File {
|
* 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) {
|
return when (type) {
|
||||||
MediaType.MANGA -> {
|
MediaType.MANGA -> {
|
||||||
if (chapter != null) {
|
base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
|
||||||
File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$mangaLocation/$title/$chapter"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$mangaLocation/$title"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaType.ANIME -> {
|
MediaType.ANIME -> {
|
||||||
if (chapter != null) {
|
base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
|
||||||
File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$animeLocation/$title/$chapter"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
File(
|
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$animeLocation/$title"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (chapter != null) {
|
base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
|
||||||
File(
|
}
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
}
|
||||||
"$novelLocation/$title/$chapter"
|
}
|
||||||
)
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
} else {
|
||||||
File(
|
baseDirectory.findOrCreateFolder(title, overwrite)
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"$novelLocation/$title"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.PackageManager
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.media3.common.util.UnstableApi
|
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.FileUrl
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.currActivity
|
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.download.video.Helper
|
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaType
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.SubtitleDownloader
|
import ani.dantotsu.media.SubtitleDownloader
|
||||||
|
@ -36,6 +33,12 @@ import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.util.Logger
|
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.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
@ -56,13 +58,12 @@ import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
class AnimeDownloaderService : Service() {
|
class AnimeDownloaderService : Service() {
|
||||||
|
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
|
@ -88,6 +89,7 @@ class AnimeDownloaderService : Service() {
|
||||||
setSmallIcon(R.drawable.ic_download_24)
|
setSmallIcon(R.drawable.ic_download_24)
|
||||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
|
setProgress(100, 0, false)
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
startForeground(
|
startForeground(
|
||||||
|
@ -156,27 +158,14 @@ class AnimeDownloaderService : Service() {
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun cancelDownload(taskName: String) {
|
fun cancelDownload(taskName: String) {
|
||||||
val url =
|
val sessionIds =
|
||||||
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
|
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
|
||||||
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
.map { it.sessionId }.toMutableList()
|
||||||
if (url.isEmpty()) {
|
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
||||||
snackString("Failed to cancel download")
|
sessionIds.forEach {
|
||||||
return
|
FFmpegKit.cancel(it)
|
||||||
}
|
}
|
||||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
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 {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
downloadJobs[taskName]?.cancel()
|
downloadJobs[taskName]?.cancel()
|
||||||
|
@ -209,7 +198,7 @@ class AnimeDownloaderService : Service() {
|
||||||
@androidx.annotation.OptIn(UnstableApi::class)
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
suspend fun download(task: AnimeDownloadTask) {
|
suspend fun download(task: AnimeDownloadTask) {
|
||||||
try {
|
try {
|
||||||
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
//val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
|
@ -220,18 +209,80 @@ class AnimeDownloaderService : Service() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
|
||||||
if (notifi) {
|
if (notifi) {
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
currActivity()?.let {
|
val outputDir = getSubDirectory(
|
||||||
Helper.downloadVideo(
|
this@AnimeDownloaderService,
|
||||||
it,
|
MediaType.ANIME,
|
||||||
task.video,
|
false,
|
||||||
task.subtitle
|
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)
|
saveMediaInfo(task)
|
||||||
task.subtitle?.let {
|
task.subtitle?.let {
|
||||||
|
@ -245,40 +296,33 @@ 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
|
// periodically check if the download is complete
|
||||||
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
while (ffTask.state != SessionState.COMPLETED) {
|
||||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
if (ffTask.state == SessionState.FAILED) {
|
||||||
if (download != null) {
|
|
||||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
|
||||||
Logger.log("Download failed")
|
Logger.log("Download failed")
|
||||||
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
builder.setContentText(
|
||||||
|
"${
|
||||||
|
getTaskName(
|
||||||
|
task.title,
|
||||||
|
task.episode
|
||||||
|
)
|
||||||
|
} Download failed"
|
||||||
|
)
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
snackString("${task.title} - ${task.episode} Download failed")
|
snackString("${getTaskName(task.title, task.episode)} Download failed")
|
||||||
Logger.log("Download failed: ${download.failureReason}")
|
Logger.log("Download failed: ${ffTask.failStackTrace}")
|
||||||
downloadsManager.removeDownload(
|
downloadsManager.removeDownload(
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.episode,
|
task.episode,
|
||||||
MediaType.ANIME,
|
MediaType.ANIME,
|
||||||
)
|
)
|
||||||
)
|
) {}
|
||||||
Injekt.get<CrashlyticsInterface>().logException(
|
Injekt.get<CrashlyticsInterface>().logException(
|
||||||
Exception(
|
Exception(
|
||||||
"Anime Download failed:" +
|
"Anime Download failed:" +
|
||||||
" ${download.failureReason}" +
|
" ${getTaskName(task.title, task.episode)}" +
|
||||||
" url: ${task.video.file.url}" +
|
" url: ${task.video.file.url}" +
|
||||||
" title: ${task.title}" +
|
" title: ${task.title}" +
|
||||||
" episode: ${task.episode}"
|
" episode: ${task.episode}"
|
||||||
|
@ -288,11 +332,63 @@ class AnimeDownloaderService : Service() {
|
||||||
broadcastDownloadFailed(task.episode)
|
broadcastDownloadFailed(task.episode)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
|
builder.setProgress(
|
||||||
Logger.log("Download completed")
|
100, percent.coerceAtMost(99),
|
||||||
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
false
|
||||||
|
)
|
||||||
|
broadcastDownloadProgress(
|
||||||
|
task.episode,
|
||||||
|
percent.coerceAtMost(99)
|
||||||
|
)
|
||||||
|
if (notifi) {
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
snackString("${task.title} - ${task.episode} Download completed")
|
}
|
||||||
|
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(
|
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||||
task.getTaskName(),
|
task.getTaskName(),
|
||||||
task.video.file.url
|
task.video.file.url
|
||||||
|
@ -304,27 +400,11 @@ class AnimeDownloaderService : Service() {
|
||||||
MediaType.ANIME,
|
MediaType.ANIME,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
broadcastDownloadFinished(task.episode)
|
broadcastDownloadFinished(task.episode)
|
||||||
break
|
} else throw Exception("Download failed")
|
||||||
}
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
if (notifi) {
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
kotlinx.coroutines.delay(2000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
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) {
|
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val directory = File(
|
val directory =
|
||||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
|
||||||
"${DownloadsManager.animeLocation}/${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
|
||||||
)
|
)
|
||||||
val episodeDirectory = File(directory, task.episode)
|
?: throw Exception("Directory not found")
|
||||||
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
|
||||||
|
|
||||||
val file = File(directory, "media.json")
|
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
@ -399,14 +468,25 @@ class AnimeDownloaderService : Service() {
|
||||||
|
|
||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
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: DocumentFile, name: String): String? =
|
||||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
var connection: HttpURLConnection? = null
|
var connection: HttpURLConnection? = null
|
||||||
println("Downloading url $url")
|
println("Downloading url $url")
|
||||||
|
@ -417,13 +497,16 @@ class AnimeDownloaderService : Service() {
|
||||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = File(directory, name)
|
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
|
||||||
FileOutputStream(file).use { output ->
|
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 ->
|
connection.inputStream.use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@withContext file.absolutePath
|
return@withContext file.uri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
@ -490,14 +573,15 @@ class AnimeDownloaderService : Service() {
|
||||||
val episodeImage: String? = null,
|
val episodeImage: String? = null,
|
||||||
val retries: Int = 2,
|
val retries: Int = 2,
|
||||||
val simultaneousDownloads: Int = 2,
|
val simultaneousDownloads: Int = 2,
|
||||||
|
var sessionId: Long = -1
|
||||||
) {
|
) {
|
||||||
fun getTaskName(): String {
|
fun getTaskName(): String {
|
||||||
return "$title - $episode"
|
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getTaskName(title: String, episode: String): String {
|
fun getTaskName(title: String, episode: String): String {
|
||||||
return "$title - $episode"
|
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -511,7 +595,6 @@ class AnimeDownloaderService : Service() {
|
||||||
|
|
||||||
object AnimeServiceDataSingleton {
|
object AnimeServiceDataSingleton {
|
||||||
var video: Video? = null
|
var video: Video? = null
|
||||||
var sourceMedia: Media? = null
|
|
||||||
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|
|
@ -4,7 +4,6 @@ package ani.dantotsu.download.anime
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
@ -25,6 +24,7 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginBottom
|
import androidx.core.view.marginBottom
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.bottomBar
|
import ani.dantotsu.bottomBar
|
||||||
|
@ -33,6 +33,7 @@ import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
@ -44,6 +45,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
|
import com.anggrayudi.storage.file.openInputStream
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
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.animesource.model.SEpisodeImpl
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
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.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
|
|
||||||
|
@ -66,6 +72,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
private lateinit var gridView: GridView
|
private lateinit var gridView: GridView
|
||||||
private lateinit var adapter: OfflineAnimeAdapter
|
private lateinit var adapter: OfflineAnimeAdapter
|
||||||
private lateinit var total: TextView
|
private lateinit var total: TextView
|
||||||
|
private var downloadsJob: Job = Job()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -112,10 +119,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
})
|
})
|
||||||
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
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) {
|
var selected = when (style) {
|
||||||
0 -> layoutList
|
0 -> layoutList
|
||||||
1 -> layoutcompact
|
1 -> layoutCompact
|
||||||
else -> layoutList
|
else -> layoutList
|
||||||
}
|
}
|
||||||
selected.alpha = 1f
|
selected.alpha = 1f
|
||||||
|
@ -136,7 +143,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
grid()
|
grid()
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutcompact.setOnClickListener {
|
layoutCompact.setOnClickListener {
|
||||||
selected(it as ImageView)
|
selected(it as ImageView)
|
||||||
style = 1
|
style = 1
|
||||||
PrefManager.setVal(PrefName.OfflineView, style)
|
PrefManager.setVal(PrefName.OfflineView, style)
|
||||||
|
@ -156,11 +163,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun grid() {
|
private fun grid() {
|
||||||
gridView.visibility = View.VISIBLE
|
gridView.visibility = View.VISIBLE
|
||||||
getDownloads()
|
|
||||||
val fadeIn = AlphaAnimation(0f, 1f)
|
val fadeIn = AlphaAnimation(0f, 1f)
|
||||||
fadeIn.duration = 300 // animations pog
|
fadeIn.duration = 300 // animations pog
|
||||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||||
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
||||||
|
getDownloads()
|
||||||
gridView.adapter = adapter
|
gridView.adapter = adapter
|
||||||
gridView.scheduleLayoutAnimation()
|
gridView.scheduleLayoutAnimation()
|
||||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||||
|
@ -168,12 +175,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
// Get the OfflineAnimeModel that was clicked
|
// Get the OfflineAnimeModel that was clicked
|
||||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||||
val media =
|
val media =
|
||||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
|
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title.findValidName() }
|
||||||
media?.let {
|
media?.let {
|
||||||
|
lifecycleScope.launch {
|
||||||
val mediaModel = getMedia(it)
|
val mediaModel = getMedia(it)
|
||||||
if (mediaModel == null) {
|
if (mediaModel == null) {
|
||||||
snackString("Error loading media.json")
|
snackString("Error loading media.json")
|
||||||
return@let
|
return@launch
|
||||||
}
|
}
|
||||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
|
@ -182,6 +190,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
.putExtra("download", true),
|
.putExtra("download", true),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
snackString("no media found")
|
snackString("no media found")
|
||||||
}
|
}
|
||||||
|
@ -204,13 +213,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
if (mediaIds.isEmpty()) {
|
if (mediaIds.isEmpty()) {
|
||||||
snackString("No media found") // if this happens, terrible things have happened
|
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()
|
getDownloads()
|
||||||
adapter.setItems(downloads)
|
|
||||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
|
||||||
}
|
}
|
||||||
builder.setNegativeButton("No") { _, _ ->
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -238,7 +241,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
|
|
||||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||||
// Implement behavior for different scroll states if needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
|
@ -261,7 +263,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
getDownloads()
|
getDownloads()
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
@ -281,6 +282,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
|
|
||||||
private fun getDownloads() {
|
private fun getDownloads() {
|
||||||
downloads = listOf()
|
downloads = listOf()
|
||||||
|
if (downloadsJob.isActive) {
|
||||||
|
downloadsJob.cancel()
|
||||||
|
}
|
||||||
|
downloadsJob = Job()
|
||||||
|
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||||
for (title in animeTitles) {
|
for (title in animeTitles) {
|
||||||
|
@ -290,16 +296,25 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
newAnimeDownloads += offlineAnimeModel
|
newAnimeDownloads += offlineAnimeModel
|
||||||
}
|
}
|
||||||
downloads = newAnimeDownloads
|
downloads = newAnimeDownloads
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
adapter.setItems(downloads)
|
||||||
|
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
/**
|
||||||
val type = downloadedType.type.asText()
|
* Load media.json file from the directory and convert it to Media class
|
||||||
val directory = File(
|
* @param downloadedType DownloadedType object
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
* @return Media object
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
*/
|
||||||
)
|
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
//load media.json and convert to media class with gson
|
|
||||||
return try {
|
return try {
|
||||||
|
val directory = DownloadsManager.getSubDirectory(
|
||||||
|
context ?: currContext()!!, downloadedType.type,
|
||||||
|
false, downloadedType.title
|
||||||
|
)
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
@ -311,8 +326,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||||
})
|
})
|
||||||
.create()
|
.create()
|
||||||
val media = File(directory, "media.json")
|
val media = directory?.findFile("media.json")
|
||||||
val mediaJson = media.readText()
|
?: return null
|
||||||
|
val mediaJson =
|
||||||
|
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||||
|
it?.readText()
|
||||||
|
}
|
||||||
|
?: return null
|
||||||
gson.fromJson(mediaJson, Media::class.java)
|
gson.fromJson(mediaJson, Media::class.java)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error loading media.json: ${e.message}")
|
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 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 {
|
try {
|
||||||
|
val directory = DownloadsManager.getSubDirectory(
|
||||||
|
context ?: currContext()!!, downloadedType.type,
|
||||||
|
false, downloadedType.title
|
||||||
|
)
|
||||||
val mediaModel = getMedia(downloadedType)!!
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
val cover = File(directory, "cover.jpg")
|
val cover = directory?.findFile("cover.jpg")
|
||||||
val coverUri: Uri? = if (cover.exists()) {
|
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||||
Uri.fromFile(cover)
|
cover.uri
|
||||||
} else null
|
} else null
|
||||||
val banner = File(directory, "banner.jpg")
|
val banner = directory?.findFile("banner.jpg")
|
||||||
val bannerUri: Uri? = if (banner.exists()) {
|
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||||
Uri.fromFile(banner)
|
banner.uri
|
||||||
} else null
|
} else null
|
||||||
val title = mediaModel.mainName()
|
val title = mediaModel.mainName()
|
||||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||||
|
|
|
@ -10,17 +10,18 @@ import android.content.pm.PackageManager
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaType
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.manga.ImageData
|
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.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.util.Logger
|
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.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
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 tachiyomi.core.util.lang.launchIO
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
|
@ -189,13 +191,20 @@ class MangaDownloaderService : Service() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
|
||||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||||
if (notifi) {
|
if (notifi) {
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
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
|
// Loop through each ImageData object from the task
|
||||||
var farthest = 0
|
var farthest = 0
|
||||||
for ((index, image) in task.imageData.withIndex()) {
|
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) {
|
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||||
try {
|
try {
|
||||||
// Define the directory within the private external storage space
|
// Define the directory within the private external storage space
|
||||||
val directory = File(
|
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
|
||||||
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
?: throw Exception("Directory not found")
|
||||||
"Dantotsu/Manga/$title/$chapter"
|
directory.findFile(fileName)?.forceDelete(this)
|
||||||
)
|
// Create a file reference within that directory for the image
|
||||||
|
val file =
|
||||||
if (!directory.exists()) {
|
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
|
||||||
directory.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a file reference within that directory for your image
|
|
||||||
val file = File(directory, fileName)
|
|
||||||
|
|
||||||
// Use a FileOutputStream to write the bitmap to the file
|
// 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)
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Exception while saving image: ${e.message}")
|
println("Exception while saving image: ${e.message}")
|
||||||
snackString("Exception while saving image: ${e.message}")
|
snackString("Exception while saving image: ${e.message}")
|
||||||
|
@ -291,13 +294,12 @@ class MangaDownloaderService : Service() {
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
private fun saveMediaInfo(task: DownloadTask) {
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
launchIO {
|
launchIO {
|
||||||
val directory = File(
|
val directory =
|
||||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
|
||||||
"Dantotsu/Manga/${task.title}"
|
?: throw Exception("Directory not found")
|
||||||
)
|
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
|
||||||
if (!directory.exists()) directory.mkdirs()
|
val file = directory.createFile("application/json", "media.json")
|
||||||
|
?: throw Exception("File not created")
|
||||||
val file = File(directory, "media.json")
|
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
@ -312,7 +314,10 @@ class MangaDownloaderService : Service() {
|
||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
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) {
|
} catch (e: android.system.ErrnoException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
Toast.makeText(
|
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) {
|
withContext(Dispatchers.IO) {
|
||||||
var connection: HttpURLConnection? = null
|
var connection: HttpURLConnection? = null
|
||||||
println("Downloading url $url")
|
println("Downloading url $url")
|
||||||
|
@ -337,14 +342,16 @@ class MangaDownloaderService : Service() {
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
}
|
}
|
||||||
|
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
|
||||||
val file = File(directory, name)
|
val file =
|
||||||
FileOutputStream(file).use { output ->
|
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 ->
|
connection.inputStream.use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@withContext file.absolutePath
|
return@withContext file.uri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package ani.dantotsu.download.manga
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
@ -23,6 +22,7 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginBottom
|
import androidx.core.view.marginBottom
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.bottomBar
|
import ani.dantotsu.bottomBar
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
|
@ -30,6 +30,7 @@ import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
@ -41,6 +42,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
|
import com.anggrayudi.storage.file.openInputStream
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
@ -48,9 +50,13 @@ import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
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.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
|
|
||||||
|
@ -59,6 +65,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
private lateinit var gridView: GridView
|
private lateinit var gridView: GridView
|
||||||
private lateinit var adapter: OfflineMangaAdapter
|
private lateinit var adapter: OfflineMangaAdapter
|
||||||
private lateinit var total: TextView
|
private lateinit var total: TextView
|
||||||
|
private var downloadsJob: Job = Job()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -148,11 +155,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
|
|
||||||
private fun grid() {
|
private fun grid() {
|
||||||
gridView.visibility = View.VISIBLE
|
gridView.visibility = View.VISIBLE
|
||||||
getDownloads()
|
|
||||||
val fadeIn = AlphaAnimation(0f, 1f)
|
val fadeIn = AlphaAnimation(0f, 1f)
|
||||||
fadeIn.duration = 300 // animations pog
|
fadeIn.duration = 300 // animations pog
|
||||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||||
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||||
|
getDownloads()
|
||||||
gridView.adapter = adapter
|
gridView.adapter = adapter
|
||||||
gridView.scheduleLayoutAnimation()
|
gridView.scheduleLayoutAnimation()
|
||||||
total.text =
|
total.text =
|
||||||
|
@ -164,7 +171,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||||
media?.let {
|
media?.let {
|
||||||
|
lifecycleScope.launch {
|
||||||
ContextCompat.startActivity(
|
ContextCompat.startActivity(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||||
|
@ -172,6 +179,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
.putExtra("download", true),
|
.putExtra("download", true),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
snackString("no media found")
|
snackString("no media found")
|
||||||
}
|
}
|
||||||
|
@ -194,9 +202,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
builder.setPositiveButton("Yes") { _, _ ->
|
builder.setPositiveButton("Yes") { _, _ ->
|
||||||
downloadManager.removeMedia(item.title, type)
|
downloadManager.removeMedia(item.title, type)
|
||||||
getDownloads()
|
getDownloads()
|
||||||
adapter.setItems(downloads)
|
|
||||||
total.text =
|
|
||||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
|
||||||
}
|
}
|
||||||
builder.setNegativeButton("No") { _, _ ->
|
builder.setNegativeButton("No") { _, _ ->
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -225,7 +230,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
|
|
||||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||||
// Implement behavior for different scroll states if needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
|
@ -248,7 +252,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
getDownloads()
|
getDownloads()
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
@ -268,6 +271,12 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
|
|
||||||
private fun getDownloads() {
|
private fun getDownloads() {
|
||||||
downloads = listOf()
|
downloads = listOf()
|
||||||
|
if (downloadsJob.isActive) {
|
||||||
|
downloadsJob.cancel()
|
||||||
|
}
|
||||||
|
downloads = listOf()
|
||||||
|
downloadsJob = Job()
|
||||||
|
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||||
for (title in mangaTitles) {
|
for (title in mangaTitles) {
|
||||||
|
@ -286,24 +295,38 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
newNovelDownloads += offlineMangaModel
|
newNovelDownloads += offlineMangaModel
|
||||||
}
|
}
|
||||||
downloads += newNovelDownloads
|
downloads += newNovelDownloads
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
adapter.setItems(downloads)
|
||||||
|
total.text =
|
||||||
|
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
/**
|
||||||
val type = downloadedType.type.asText()
|
* Load media.json file from the directory and convert it to Media class
|
||||||
val directory = File(
|
* @param downloadedType DownloadedType object
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
* @return Media object
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
*/
|
||||||
)
|
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
//load media.json and convert to media class with gson
|
|
||||||
return try {
|
return try {
|
||||||
|
val directory = getSubDirectory(
|
||||||
|
context ?: currContext()!!, downloadedType.type,
|
||||||
|
false, downloadedType.title
|
||||||
|
)
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
})
|
})
|
||||||
.create()
|
.create()
|
||||||
val media = File(directory, "media.json")
|
val media = directory?.findFile("media.json")
|
||||||
val mediaJson = media.readText()
|
?: return null
|
||||||
|
val mediaJson =
|
||||||
|
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||||
|
it?.readText()
|
||||||
|
}
|
||||||
gson.fromJson(mediaJson, Media::class.java)
|
gson.fromJson(mediaJson, Media::class.java)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Error loading media.json: ${e.message}")
|
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 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 and convert to media class with gson
|
||||||
try {
|
try {
|
||||||
|
val directory = getSubDirectory(
|
||||||
|
context ?: currContext()!!, downloadedType.type,
|
||||||
|
false, downloadedType.title
|
||||||
|
)
|
||||||
val mediaModel = getMedia(downloadedType)!!
|
val mediaModel = getMedia(downloadedType)!!
|
||||||
val cover = File(directory, "cover.jpg")
|
val cover = directory?.findFile("cover.jpg")
|
||||||
val coverUri: Uri? = if (cover.exists()) {
|
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||||
Uri.fromFile(cover)
|
cover.uri
|
||||||
} else null
|
} else null
|
||||||
val banner = File(directory, "banner.jpg")
|
val banner = directory?.findFile("banner.jpg")
|
||||||
val bannerUri: Uri? = if (banner.exists()) {
|
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||||
Uri.fromFile(banner)
|
banner.uri
|
||||||
} else null
|
} else null
|
||||||
val title = mediaModel.mainName()
|
val title = mediaModel.mainName()
|
||||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||||
|
@ -336,14 +359,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
val isOngoing =
|
val isOngoing =
|
||||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||||
val isUserScored = mediaModel.userScore != 0
|
val isUserScored = mediaModel.userScore != 0
|
||||||
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
val readChapter = (mediaModel.userProgress ?: "~").toString()
|
||||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||||
val chapters = " Chapters"
|
val chapters = " Chapters"
|
||||||
return OfflineMangaModel(
|
return OfflineMangaModel(
|
||||||
title,
|
title,
|
||||||
score,
|
score,
|
||||||
totalchapter,
|
totalChapter,
|
||||||
readchapter,
|
readChapter,
|
||||||
type,
|
type,
|
||||||
chapters,
|
chapters,
|
||||||
isOngoing,
|
isOngoing,
|
||||||
|
|
|
@ -16,15 +16,19 @@ import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaType
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.novel.NovelReadFragment
|
import ani.dantotsu.media.novel.NovelReadFragment
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.util.Logger
|
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.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
@ -250,24 +254,25 @@ class NovelDownloaderService : Service() {
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw IOException("Failed to download file: ${response.message}")
|
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(
|
val file = directory.createFile("application/epub+zip", "0.epub")
|
||||||
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
?: throw Exception("File not created")
|
||||||
"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()
|
|
||||||
|
|
||||||
//download cover
|
//download cover
|
||||||
task.coverUrl?.let {
|
task.coverUrl?.let {
|
||||||
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
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 responseBody = response.body
|
||||||
val totalBytes = responseBody.contentLength()
|
val totalBytes = responseBody.contentLength()
|
||||||
var downloadedBytes = 0L
|
var downloadedBytes = 0L
|
||||||
|
@ -352,13 +357,16 @@ class NovelDownloaderService : Service() {
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
private fun saveMediaInfo(task: DownloadTask) {
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
launchIO {
|
launchIO {
|
||||||
val directory = File(
|
val directory =
|
||||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
DownloadsManager.getSubDirectory(
|
||||||
"Dantotsu/Novel/${task.title}"
|
this@NovelDownloaderService,
|
||||||
)
|
MediaType.NOVEL,
|
||||||
if (!directory.exists()) directory.mkdirs()
|
false,
|
||||||
|
task.title
|
||||||
val file = File(directory, "media.json")
|
) ?: 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()
|
val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||||
SChapterImpl() // Provide an instance of SChapterImpl
|
SChapterImpl() // Provide an instance of SChapterImpl
|
||||||
|
@ -372,33 +380,47 @@ class NovelDownloaderService : Service() {
|
||||||
|
|
||||||
val jsonString = gson.toJson(media)
|
val jsonString = gson.toJson(media)
|
||||||
withContext(Dispatchers.Main) {
|
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(
|
withContext(
|
||||||
Dispatchers.IO
|
Dispatchers.IO
|
||||||
) {
|
) {
|
||||||
var connection: HttpURLConnection? = null
|
var connection: HttpURLConnection? = null
|
||||||
println("Downloading url $url")
|
Logger.log("Downloading url $url")
|
||||||
try {
|
try {
|
||||||
connection = URL(url).openConnection() as HttpURLConnection
|
connection = URL(url).openConnection() as HttpURLConnection
|
||||||
connection.connect()
|
connection.connect()
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||||
}
|
}
|
||||||
|
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
|
||||||
val file = File(directory, name)
|
val file =
|
||||||
FileOutputStream(file).use { output ->
|
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 ->
|
connection.inputStream.use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@withContext file.absolutePath
|
return@withContext file.uri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
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")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
object Helper {
|
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)
|
@OptIn(UnstableApi::class)
|
||||||
fun startAnimeDownloadService(
|
fun startAnimeDownloadService(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -225,15 +91,6 @@ object Helper {
|
||||||
.setTitle("Download Exists")
|
.setTitle("Download Exists")
|
||||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||||
.setPositiveButton("Yes") { _, _ ->
|
.setPositiveButton("Yes") { _, _ ->
|
||||||
DownloadService.sendRemoveDownload(
|
|
||||||
context,
|
|
||||||
ExoplayerDownloadService::class.java,
|
|
||||||
PrefManager.getAnimeDownloadPreferences().getString(
|
|
||||||
animeDownloadTask.getTaskName(),
|
|
||||||
""
|
|
||||||
) ?: "",
|
|
||||||
false
|
|
||||||
)
|
|
||||||
PrefManager.getAnimeDownloadPreferences().edit()
|
PrefManager.getAnimeDownloadPreferences().edit()
|
||||||
.remove(animeDownloadTask.getTaskName())
|
.remove(animeDownloadTask.getTaskName())
|
||||||
.apply()
|
.apply()
|
||||||
|
@ -243,7 +100,7 @@ object Helper {
|
||||||
episode,
|
episode,
|
||||||
MediaType.ANIME
|
MediaType.ANIME
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||||
|
@ -251,6 +108,7 @@ object Helper {
|
||||||
AnimeServiceDataSingleton.isServiceRunning = true
|
AnimeServiceDataSingleton.isServiceRunning = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.setNegativeButton("No") { _, _ -> }
|
.setNegativeButton("No") { _, _ -> }
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
|
@ -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 {
|
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
return ActivityCompat.checkSelfPermission(
|
return ActivityCompat.checkSelfPermission(
|
||||||
|
|
|
@ -13,6 +13,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
@ -53,6 +54,7 @@ import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import ani.dantotsu.util.LauncherWrapper
|
||||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -66,7 +68,7 @@ import kotlin.math.abs
|
||||||
|
|
||||||
|
|
||||||
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||||
|
lateinit var launcher: LauncherWrapper
|
||||||
lateinit var binding: ActivityMediaBinding
|
lateinit var binding: ActivityMediaBinding
|
||||||
private val scope = lifecycleScope
|
private val scope = lifecycleScope
|
||||||
private val model: MediaDetailsViewModel by viewModels()
|
private val model: MediaDetailsViewModel by viewModels()
|
||||||
|
@ -92,6 +94,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val contract = ActivityResultContracts.OpenDocumentTree()
|
||||||
|
launcher = LauncherWrapper(this, contract)
|
||||||
|
|
||||||
mediaSingleton = null
|
mediaSingleton = null
|
||||||
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||||
MediaSingleton.bitmap = null
|
MediaSingleton.bitmap = null
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.parsers.SubtitleType
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import com.anggrayudi.storage.file.openOutputStream
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -51,21 +52,17 @@ class SubtitleDownloader {
|
||||||
downloadedType: DownloadedType
|
downloadedType: DownloadedType
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val directory = DownloadsManager.getDirectory(
|
val directory = DownloadsManager.getSubDirectory(
|
||||||
context,
|
context,
|
||||||
downloadedType.type,
|
downloadedType.type,
|
||||||
|
false,
|
||||||
downloadedType.title,
|
downloadedType.title,
|
||||||
downloadedType.chapter
|
downloadedType.chapter
|
||||||
)
|
) ?: throw Exception("Could not create directory")
|
||||||
if (!directory.exists()) { //just in case
|
|
||||||
directory.mkdirs()
|
|
||||||
}
|
|
||||||
val type = loadSubtitleType(url)
|
val type = loadSubtitleType(url)
|
||||||
val subtiteFile = File(directory, "subtitle.${type}")
|
directory.findFile("subtitle.${type}")?.delete()
|
||||||
if (subtiteFile.exists()) {
|
val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
|
||||||
subtiteFile.delete()
|
?: throw Exception("Could not create subtitle file")
|
||||||
}
|
|
||||||
subtiteFile.createNewFile()
|
|
||||||
|
|
||||||
val client = Injekt.get<NetworkHelper>().client
|
val client = Injekt.get<NetworkHelper>().client
|
||||||
val request = Request.Builder().url(url).build()
|
val request = Request.Builder().url(url).build()
|
||||||
|
@ -77,7 +74,8 @@ class SubtitleDownloader {
|
||||||
}
|
}
|
||||||
|
|
||||||
reponse.body.byteStream().use { input ->
|
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)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.math.MathUtils
|
import androidx.core.math.MathUtils
|
||||||
|
@ -34,8 +35,8 @@ import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
|
||||||
import ani.dantotsu.dp
|
import ani.dantotsu.dp
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
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.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
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 com.google.android.material.appbar.AppBarLayout
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
|
@ -422,7 +425,19 @@ class AnimeWatchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAnimeEpisodeDownloadClick(i: String) {
|
fun onAnimeEpisodeDownloadClick(i: String) {
|
||||||
|
activity?.let{
|
||||||
|
if (!hasDirAccess(it)) {
|
||||||
|
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
|
||||||
|
if (success) {
|
||||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||||
|
} else {
|
||||||
|
snackString("Permission is required to download")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
fun onAnimeEpisodeStopDownloadClick(i: String) {
|
||||||
|
@ -442,9 +457,10 @@ class AnimeWatchFragment : Fragment() {
|
||||||
i,
|
i,
|
||||||
MediaType.ANIME
|
MediaType.ANIME
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
episodeAdapter.purgeDownload(i)
|
episodeAdapter.purgeDownload(i)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun onAnimeEpisodeRemoveDownloadClick(i: String) {
|
fun onAnimeEpisodeRemoveDownloadClick(i: String) {
|
||||||
|
@ -454,21 +470,16 @@ class AnimeWatchFragment : Fragment() {
|
||||||
i,
|
i,
|
||||||
MediaType.ANIME
|
MediaType.ANIME
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
||||||
taskName,
|
taskName,
|
||||||
""
|
""
|
||||||
) ?: ""
|
) ?: ""
|
||||||
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
|
||||||
DownloadService.sendRemoveDownload(
|
|
||||||
requireContext(),
|
|
||||||
ExoplayerDownloadService::class.java,
|
|
||||||
id,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
episodeAdapter.deleteDownload(i)
|
episodeAdapter.deleteDownload(i)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
@ -531,7 +542,7 @@ class AnimeWatchFragment : Fragment() {
|
||||||
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
||||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||||
for (download in downloadManager.animeDownloadedTypes) {
|
for (download in downloadManager.animeDownloadedTypes) {
|
||||||
if (download.title == media.mainName()) {
|
if (download.title == media.mainName().findValidName()) {
|
||||||
episodeAdapter.stopDownload(download.chapter)
|
episodeAdapter.stopDownload(download.chapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import androidx.annotation.OptIn
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.offline.DownloadIndex
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.updateProgress
|
import ani.dantotsu.connections.updateProgress
|
||||||
|
@ -18,10 +17,12 @@ import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
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.anime.AnimeDownloaderService
|
||||||
import ani.dantotsu.download.video.Helper
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaNameAdapter
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.setAnimation
|
import ani.dantotsu.setAnimation
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
@ -56,15 +57,7 @@ class EpisodeAdapter(
|
||||||
var arr: List<Episode> = arrayListOf(),
|
var arr: List<Episode> = arrayListOf(),
|
||||||
var offlineMode: Boolean
|
var offlineMode: Boolean
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
val context = fragment.requireContext()
|
||||||
private lateinit var index: DownloadIndex
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (offlineMode) {
|
|
||||||
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return (when (viewType) {
|
return (when (viewType) {
|
||||||
|
@ -248,17 +241,8 @@ class EpisodeAdapter(
|
||||||
// Find the position of the chapter and notify only that item
|
// Find the position of the chapter and notify only that item
|
||||||
val position = arr.indexOfFirst { it.number == episodeNumber }
|
val position = arr.indexOfFirst { it.number == episodeNumber }
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
|
|
||||||
media.mainName(),
|
|
||||||
episodeNumber
|
|
||||||
)
|
|
||||||
val id = PrefManager.getAnimeDownloadPreferences().getString(
|
|
||||||
taskName,
|
|
||||||
""
|
|
||||||
) ?: ""
|
|
||||||
val size = try {
|
val size = try {
|
||||||
val download = index.getDownload(id)
|
bytesToHuman(getDirSize(context, MediaType.ANIME, media.mainName(), episodeNumber))
|
||||||
bytesToHuman(download?.bytesDownloaded ?: 0)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,7 @@ import ani.dantotsu.connections.discord.RPC
|
||||||
import ani.dantotsu.connections.updateProgress
|
import ani.dantotsu.connections.updateProgress
|
||||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||||
import ani.dantotsu.defaultHeaders
|
import ani.dantotsu.defaultHeaders
|
||||||
import ani.dantotsu.download.video.Helper
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.dp
|
import ani.dantotsu.dp
|
||||||
import ani.dantotsu.getCurrentBrightnessValue
|
import ani.dantotsu.getCurrentBrightnessValue
|
||||||
import ani.dantotsu.hideSystemBars
|
import ani.dantotsu.hideSystemBars
|
||||||
|
@ -114,6 +114,7 @@ import ani.dantotsu.logError
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.media.MediaNameAdapter
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.SubtitleDownloader
|
import ani.dantotsu.media.SubtitleDownloader
|
||||||
import ani.dantotsu.okHttpClient
|
import ani.dantotsu.okHttpClient
|
||||||
import ani.dantotsu.others.AniSkip
|
import ani.dantotsu.others.AniSkip
|
||||||
|
@ -394,7 +395,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
isCastApiAvailable = GoogleApiAvailability.getInstance()
|
isCastApiAvailable = GoogleApiAvailability.getInstance()
|
||||||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||||
try {
|
try {
|
||||||
castContext = CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
|
castContext =
|
||||||
|
CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
|
||||||
castPlayer = CastPlayer(castContext!!)
|
castPlayer = CastPlayer(castContext!!)
|
||||||
castPlayer!!.setSessionAvailabilityListener(this)
|
castPlayer!!.setSessionAvailabilityListener(this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -453,12 +455,14 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
}
|
}
|
||||||
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
}
|
}
|
||||||
|
|
||||||
in 225..315 -> {
|
in 225..315 -> {
|
||||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
||||||
exoRotate.visibility = View.VISIBLE
|
exoRotate.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
}
|
}
|
||||||
|
|
||||||
in 315..360, in 0..45 -> {
|
in 315..360, in 0..45 -> {
|
||||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
|
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
|
||||||
exoRotate.visibility = View.VISIBLE
|
exoRotate.visibility = View.VISIBLE
|
||||||
|
@ -476,7 +480,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
requestedOrientation = rotation
|
requestedOrientation = rotation
|
||||||
it.visibility = View.GONE
|
it.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSubFormatting(playerView)
|
setupSubFormatting(playerView)
|
||||||
|
|
||||||
|
@ -1089,10 +1093,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
"nothing" -> mutableListOf(
|
"nothing" -> mutableListOf(
|
||||||
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
"dantotsu" -> mutableListOf(
|
"dantotsu" -> mutableListOf(
|
||||||
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
||||||
RPC.Link("Watch on Dantotsu", getString(R.string.dantotsu))
|
RPC.Link("Watch on Dantotsu", getString(R.string.dantotsu))
|
||||||
)
|
)
|
||||||
|
|
||||||
"anilist" -> {
|
"anilist" -> {
|
||||||
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
|
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
|
||||||
val anilistLink = "https://anilist.co/user/$userId/"
|
val anilistLink = "https://anilist.co/user/$userId/"
|
||||||
|
@ -1101,6 +1107,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
RPC.Link("View My AniList", anilistLink)
|
RPC.Link("View My AniList", anilistLink)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> mutableListOf()
|
else -> mutableListOf()
|
||||||
}
|
}
|
||||||
val presence = RPC.createPresence(
|
val presence = RPC.createPresence(
|
||||||
|
@ -1113,7 +1120,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
ep.number
|
ep.number
|
||||||
),
|
),
|
||||||
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
|
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),
|
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
|
||||||
buttons = buttons
|
buttons = buttons
|
||||||
)
|
)
|
||||||
|
@ -1161,7 +1173,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
if (PrefManager.getVal(PrefName.Cast)) {
|
if (PrefManager.getVal(PrefName.Cast)) {
|
||||||
playerView.findViewById<CustomCastButton>(R.id.exo_cast).apply {
|
playerView.findViewById<CustomCastButton>(R.id.exo_cast).apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
if(PrefManager.getVal(PrefName.UseInternalCast)) {
|
if (PrefManager.getVal(PrefName.UseInternalCast)) {
|
||||||
try {
|
try {
|
||||||
CastButtonFactory.setUpMediaRouteButton(context, this)
|
CastButtonFactory.setUpMediaRouteButton(context, this)
|
||||||
dialogFactory = CustomCastThemeFactory()
|
dialogFactory = CustomCastThemeFactory()
|
||||||
|
@ -1324,7 +1336,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@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)
|
if (list.contains(media.id)) list.remove(media.id)
|
||||||
list.add(media.id)
|
list.add(media.id)
|
||||||
PrefManager.setCustomVal("continueAnimeList", list)
|
PrefManager.setCustomVal("continueAnimeList", list)
|
||||||
|
@ -1418,7 +1434,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
}
|
}
|
||||||
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
|
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
|
||||||
cacheFactory = CacheDataSource.Factory().apply {
|
cacheFactory = CacheDataSource.Factory().apply {
|
||||||
setCache(Helper.getSimpleCache(this@ExoplayerView))
|
setCache(VideoCache.getInstance(this@ExoplayerView))
|
||||||
if (ext.server.offline) {
|
if (ext.server.offline) {
|
||||||
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
|
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1435,15 +1451,28 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
|
|
||||||
val downloadedMediaItem = if (ext.server.offline) {
|
val downloadedMediaItem = if (ext.server.offline) {
|
||||||
val key = ext.server.name
|
val key = ext.server.name
|
||||||
downloadId = PrefManager.getAnimeDownloadPreferences()
|
val titleName = ext.server.name.split("/").first()
|
||||||
.getString(key, null)
|
val episodeName = ext.server.name.split("/").last()
|
||||||
if (downloadId != null) {
|
|
||||||
Helper.downloadManager(this)
|
val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
|
||||||
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
|
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 {
|
} else {
|
||||||
snackString("Download not found")
|
snackString("File not found")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
snackString("Directory not found")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
mediaItem = if (downloadedMediaItem == null) {
|
mediaItem = if (downloadedMediaItem == null) {
|
||||||
|
@ -1818,7 +1847,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
|
|
||||||
if (!functionstarted && !disappeared && PrefManager.getVal(PrefName.AutoHideTimeStamps)) {
|
if (!functionstarted && !disappeared && PrefManager.getVal(PrefName.AutoHideTimeStamps)) {
|
||||||
disappearSkip()
|
disappearSkip()
|
||||||
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)){
|
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)) {
|
||||||
skipTimeButton.visibility = View.VISIBLE
|
skipTimeButton.visibility = View.VISIBLE
|
||||||
exoSkip.visibility = View.GONE
|
exoSkip.visibility = View.GONE
|
||||||
skipTimeText.text = new.skipType.getType()
|
skipTimeText.text = new.skipType.getType()
|
||||||
|
@ -2157,11 +2186,16 @@ class CustomCastButton : MediaRouteButton {
|
||||||
fun setCastCallback(castCallback: () -> Unit) {
|
fun setCastCallback(castCallback: () -> Unit) {
|
||||||
this.castCallback = castCallback
|
this.castCallback = castCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
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 {
|
override fun performClick(): Boolean {
|
||||||
return if (PrefManager.getVal(PrefName.UseInternalCast)) {
|
return if (PrefManager.getVal(PrefName.UseInternalCast)) {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -34,6 +35,7 @@ import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
|
||||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||||
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||||
import ani.dantotsu.dp
|
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.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
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 com.google.android.material.appbar.AppBarLayout
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
@ -190,7 +194,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
)
|
)
|
||||||
|
|
||||||
for (download in downloadManager.mangaDownloadedTypes) {
|
for (download in downloadManager.mangaDownloadedTypes) {
|
||||||
if (download.title == media.mainName()) {
|
if (download.title == media.mainName().findValidName()) {
|
||||||
chapterAdapter.stopDownload(download.chapter)
|
chapterAdapter.stopDownload(download.chapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,16 +438,17 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMangaChapterDownloadClick(i: String) {
|
fun onMangaChapterDownloadClick(i: String) {
|
||||||
|
activity?.let {
|
||||||
if (!isNotificationPermissionGranted()) {
|
if (!isNotificationPermissionGranted()) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
ActivityCompat.requestPermissions(
|
ActivityCompat.requestPermissions(
|
||||||
requireActivity(),
|
it,
|
||||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun continueDownload() {
|
||||||
model.continueMedia = false
|
model.continueMedia = false
|
||||||
media.manga?.chapters?.get(i)?.let { chapter ->
|
media.manga?.chapters?.get(i)?.let { chapter ->
|
||||||
val parser =
|
val parser =
|
||||||
|
@ -481,6 +486,19 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!hasDirAccess(it)) {
|
||||||
|
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
|
||||||
|
if (success) {
|
||||||
|
continueDownload()
|
||||||
|
} else {
|
||||||
|
snackString("Permission is required to download")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continueDownload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isNotificationPermissionGranted(): Boolean {
|
private fun isNotificationPermissionGranted(): Boolean {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
@ -500,9 +518,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
i,
|
i,
|
||||||
MediaType.MANGA
|
MediaType.MANGA
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
chapterAdapter.deleteDownload(i)
|
chapterAdapter.deleteDownload(i)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onMangaChapterStopDownloadClick(i: String) {
|
fun onMangaChapterStopDownloadClick(i: String) {
|
||||||
val cancelIntent = Intent().apply {
|
val cancelIntent = Intent().apply {
|
||||||
|
@ -518,9 +537,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
i,
|
i,
|
||||||
MediaType.MANGA
|
MediaType.MANGA
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
chapterAdapter.purgeDownload(i)
|
chapterAdapter.purgeDownload(i)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.net.Uri
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -176,6 +177,10 @@ abstract class BaseImageAdapter(
|
||||||
it.load(localFile.absoluteFile)
|
it.load(localFile.absoluteFile)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
} else if (link.url.startsWith("content://")) {
|
||||||
|
it.load(Uri.parse(link.url))
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
} else {
|
} else {
|
||||||
mangaCache.get(link.url)?.let { imageData ->
|
mangaCache.get(link.url)?.let { imageData ->
|
||||||
val bitmap = imageData.fetchAndProcessImage(
|
val bitmap = imageData.fetchAndProcessImage(
|
||||||
|
@ -186,6 +191,7 @@ abstract class BaseImageAdapter(
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?.let {
|
?.let {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
@ -94,16 +95,12 @@ class NovelReadFragment : Fragment(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val file = File(
|
try {
|
||||||
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
val directory =
|
||||||
"${DownloadsManager.novelLocation}/${media.mainName()}/${novel.name}/0.epub"
|
DownloadsManager.getSubDirectory(context?:currContext()!!, MediaType.NOVEL, false, novel.name)
|
||||||
)
|
val file = directory?.findFile(novel.name)
|
||||||
if (!file.exists()) return false
|
if (file?.exists() == false) return false
|
||||||
val fileUri = FileProvider.getUriForFile(
|
val fileUri = file?.uri ?: return false
|
||||||
requireContext(),
|
|
||||||
"${requireContext().packageName}.provider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
val intent = Intent(context, NovelReaderActivity::class.java).apply {
|
val intent = Intent(context, NovelReaderActivity::class.java).apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
setDataAndType(fileUri, "application/epub+zip")
|
setDataAndType(fileUri, "application/epub+zip")
|
||||||
|
@ -111,6 +108,10 @@ class NovelReadFragment : Fragment(),
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
return true
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.log(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -135,7 +136,7 @@ class NovelReadFragment : Fragment(),
|
||||||
novel.name,
|
novel.name,
|
||||||
MediaType.NOVEL
|
MediaType.NOVEL
|
||||||
)
|
)
|
||||||
)
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||||
|
|
|
@ -46,11 +46,11 @@ class CommentNotificationTask : Task {
|
||||||
)
|
)
|
||||||
|
|
||||||
notifications =
|
notifications =
|
||||||
notifications?.filter { it.type != 3 || it.notificationId > recentGlobal }
|
notifications?.filter { !it.type.isGlobal() || it.notificationId > recentGlobal }
|
||||||
?.toMutableList()
|
?.toMutableList()
|
||||||
|
|
||||||
val newRecentGlobal =
|
val newRecentGlobal =
|
||||||
notifications?.filter { it.type == 3 }?.maxOfOrNull { it.notificationId }
|
notifications?.filter { it.type.isGlobal() }?.maxOfOrNull { it.notificationId }
|
||||||
if (newRecentGlobal != null) {
|
if (newRecentGlobal != null) {
|
||||||
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
|
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
|
||||||
}
|
}
|
||||||
|
@ -313,4 +313,6 @@ class CommentNotificationTask : Task {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Int?.isGlobal() = this == 3 || this == 420
|
||||||
}
|
}
|
|
@ -36,16 +36,8 @@ object Download {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDownloadDir(context: Context): File {
|
private fun getDownloadDir(context: Context): File {
|
||||||
val direct: File
|
val direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
|
||||||
if (PrefManager.getVal(PrefName.SdDl)) {
|
|
||||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
|
|
||||||
val parentDirectory = arrayOfFiles[1].toString()
|
|
||||||
direct = File(parentDirectory)
|
|
||||||
if (!direct.exists()) direct.mkdirs()
|
if (!direct.exists()) direct.mkdirs()
|
||||||
} else {
|
|
||||||
direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
|
|
||||||
if (!direct.exists()) direct.mkdirs()
|
|
||||||
}
|
|
||||||
return direct
|
return direct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,49 +88,7 @@ object Download {
|
||||||
when (PrefManager.getVal(PrefName.DownloadManager) as Int) {
|
when (PrefManager.getVal(PrefName.DownloadManager) as Int) {
|
||||||
1 -> oneDM(context, file, notif ?: fileName)
|
1 -> oneDM(context, file, notif ?: fileName)
|
||||||
2 -> adm(context, file, fileName, folder)
|
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ import ani.dantotsu.BottomSheetDialogFragment
|
||||||
import ani.dantotsu.FileUrl
|
import ani.dantotsu.FileUrl
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.BottomSheetImageBinding
|
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.loadBitmap
|
||||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmapOld
|
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmapOld
|
||||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
||||||
|
@ -22,6 +21,7 @@ import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.shareImage
|
import ani.dantotsu.shareImage
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
|
import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package ani.dantotsu.parsers
|
package ani.dantotsu.parsers
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.download.DownloadsManager
|
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.MediaType
|
||||||
import ani.dantotsu.media.MediaNameAdapter
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
|
@ -18,6 +21,7 @@ import java.util.Locale
|
||||||
|
|
||||||
class OfflineAnimeParser : AnimeParser() {
|
class OfflineAnimeParser : AnimeParser() {
|
||||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
private val context = Injekt.get<Application>()
|
||||||
|
|
||||||
override val name = "Offline"
|
override val name = "Offline"
|
||||||
override val saveName = "Offline"
|
override val saveName = "Offline"
|
||||||
|
@ -29,22 +33,19 @@ class OfflineAnimeParser : AnimeParser() {
|
||||||
extra: Map<String, String>?,
|
extra: Map<String, String>?,
|
||||||
sAnime: SAnime
|
sAnime: SAnime
|
||||||
): List<Episode> {
|
): List<Episode> {
|
||||||
val directory = File(
|
val directory = getSubDirectory(context, MediaType.ANIME, false, animeLink)
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"${DownloadsManager.animeLocation}/$animeLink"
|
|
||||||
)
|
|
||||||
//get all of the folder names and add them to the list
|
//get all of the folder names and add them to the list
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = mutableListOf<Episode>()
|
||||||
if (directory.exists()) {
|
if (directory?.exists() == true) {
|
||||||
directory.listFiles()?.forEach {
|
directory.listFiles().forEach {
|
||||||
//put the title and episdode number in the extra data
|
//put the title and episdode number in the extra data
|
||||||
val extraData = mutableMapOf<String, String>()
|
val extraData = mutableMapOf<String, String>()
|
||||||
extraData["title"] = animeLink
|
extraData["title"] = animeLink
|
||||||
extraData["episode"] = it.name
|
extraData["episode"] = it.name!!
|
||||||
if (it.isDirectory) {
|
if (it.isDirectory) {
|
||||||
val episode = Episode(
|
val episode = Episode(
|
||||||
it.name,
|
it.name!!,
|
||||||
"$animeLink - ${it.name}",
|
getTaskName(animeLink,it.name!!),
|
||||||
it.name,
|
it.name,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
@ -131,18 +132,19 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
|
|
||||||
private fun getSubtitle(title: String, episode: String): List<Subtitle>? {
|
private fun getSubtitle(title: String, episode: String): List<Subtitle>? {
|
||||||
currContext()?.let {
|
currContext()?.let {
|
||||||
DownloadsManager.getDirectory(
|
DownloadsManager.getSubDirectory(
|
||||||
it,
|
it,
|
||||||
MediaType.ANIME,
|
MediaType.ANIME,
|
||||||
|
false,
|
||||||
title,
|
title,
|
||||||
episode
|
episode
|
||||||
).listFiles()?.forEach { file ->
|
)?.listFiles()?.forEach { file ->
|
||||||
if (file.name.contains("subtitle")) {
|
if (file.name?.contains("subtitle") == true) {
|
||||||
return listOf(
|
return listOf(
|
||||||
Subtitle(
|
Subtitle(
|
||||||
"Downloaded Subtitle",
|
"Downloaded Subtitle",
|
||||||
Uri.fromFile(file).toString(),
|
file.uri.toString(),
|
||||||
determineSubtitletype(file.absolutePath)
|
determineSubtitletype(file.name ?: "")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package ani.dantotsu.parsers
|
package ani.dantotsu.parsers
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.media.MediaNameAdapter
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
@ -14,6 +17,7 @@ import java.io.File
|
||||||
|
|
||||||
class OfflineMangaParser : MangaParser() {
|
class OfflineMangaParser : MangaParser() {
|
||||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
private val context = Injekt.get<Application>()
|
||||||
|
|
||||||
override val hostUrl: String = "Offline"
|
override val hostUrl: String = "Offline"
|
||||||
override val name: String = "Offline"
|
override val name: String = "Offline"
|
||||||
|
@ -23,17 +27,14 @@ class OfflineMangaParser : MangaParser() {
|
||||||
extra: Map<String, String>?,
|
extra: Map<String, String>?,
|
||||||
sManga: SManga
|
sManga: SManga
|
||||||
): List<MangaChapter> {
|
): List<MangaChapter> {
|
||||||
val directory = File(
|
val directory = getSubDirectory(context, MediaType.MANGA, false, mangaLink)
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/Manga/$mangaLink"
|
|
||||||
)
|
|
||||||
//get all of the folder names and add them to the list
|
//get all of the folder names and add them to the list
|
||||||
val chapters = mutableListOf<MangaChapter>()
|
val chapters = mutableListOf<MangaChapter>()
|
||||||
if (directory.exists()) {
|
if (directory?.exists() == true) {
|
||||||
directory.listFiles()?.forEach {
|
directory.listFiles().forEach {
|
||||||
if (it.isDirectory) {
|
if (it.isDirectory) {
|
||||||
val chapter = MangaChapter(
|
val chapter = MangaChapter(
|
||||||
it.name,
|
it.name!!,
|
||||||
"$mangaLink/${it.name}",
|
"$mangaLink/${it.name}",
|
||||||
it.name,
|
it.name,
|
||||||
null,
|
null,
|
||||||
|
@ -50,16 +51,15 @@ class OfflineMangaParser : MangaParser() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||||
val directory = File(
|
val title = chapterLink.split("/").first()
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
val chapter = chapterLink.split("/").last()
|
||||||
"Dantotsu/Manga/$chapterLink"
|
val directory = getSubDirectory(context, MediaType.MANGA, false, title, chapter)
|
||||||
)
|
|
||||||
val images = mutableListOf<MangaImage>()
|
val images = mutableListOf<MangaImage>()
|
||||||
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
|
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
|
||||||
if (directory.exists()) {
|
if (directory?.exists() == true) {
|
||||||
directory.listFiles()?.forEach {
|
directory.listFiles().forEach {
|
||||||
if (it.isFile) {
|
if (it.isFile) {
|
||||||
val image = MangaImage(it.absolutePath, false, null)
|
val image = MangaImage(it.uri.toString(), false, null)
|
||||||
images.add(image)
|
images.add(image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package ani.dantotsu.parsers
|
package ani.dantotsu.parsers
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.media.MediaNameAdapter
|
import ani.dantotsu.media.MediaNameAdapter
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -11,6 +14,7 @@ import java.io.File
|
||||||
|
|
||||||
class OfflineNovelParser : NovelParser() {
|
class OfflineNovelParser : NovelParser() {
|
||||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
private val context = Injekt.get<Application>()
|
||||||
|
|
||||||
override val hostUrl: String = "Offline"
|
override val hostUrl: String = "Offline"
|
||||||
override val name: 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 {
|
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
|
||||||
//link should be a directory
|
//link should be a directory
|
||||||
val directory = File(
|
val directory = getSubDirectory(context, MediaType.NOVEL, false, link)
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/Novel/$link"
|
|
||||||
)
|
|
||||||
val chapters = mutableListOf<Book>()
|
val chapters = mutableListOf<Book>()
|
||||||
if (directory.exists()) {
|
if (directory?.exists() == true) {
|
||||||
directory.listFiles()?.forEach {
|
directory.listFiles().forEach {
|
||||||
if (it.isDirectory) {
|
if (it.isDirectory) {
|
||||||
val chapter = Book(
|
val chapter = Book(
|
||||||
it.name,
|
it.name?:"Unknown",
|
||||||
it.absolutePath + "/cover.jpg",
|
it.uri.toString(),
|
||||||
null,
|
null,
|
||||||
listOf(it.absolutePath + "/0.epub")
|
listOf(it.uri.toString())
|
||||||
)
|
)
|
||||||
chapters.add(chapter)
|
chapters.add(chapter)
|
||||||
}
|
}
|
||||||
|
@ -60,20 +61,16 @@ class OfflineNovelParser : NovelParser() {
|
||||||
val returnList: MutableList<ShowResponse> = mutableListOf()
|
val returnList: MutableList<ShowResponse> = mutableListOf()
|
||||||
for (title in returnTitles) {
|
for (title in returnTitles) {
|
||||||
//need to search the subdirectories for the ShowResponses
|
//need to search the subdirectories for the ShowResponses
|
||||||
val directory = File(
|
val directory = getSubDirectory(context, MediaType.NOVEL, false, title)
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
"Dantotsu/Novel/$title"
|
|
||||||
)
|
|
||||||
val names = mutableListOf<String>()
|
val names = mutableListOf<String>()
|
||||||
if (directory.exists()) {
|
if (directory?.exists() == true) {
|
||||||
directory.listFiles()?.forEach {
|
directory.listFiles().forEach {
|
||||||
if (it.isDirectory) {
|
if (it.isDirectory) {
|
||||||
names.add(it.name)
|
names.add(it.name?: "Unknown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val cover =
|
val cover = directory?.findFile("cover.jpg")?.uri.toString()
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg"
|
|
||||||
names.forEach {
|
names.forEach {
|
||||||
returnList.add(ShowResponse(it, it, cover))
|
returnList.add(ShowResponse(it, it, cover))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.BRAND
|
import android.os.Build.BRAND
|
||||||
import android.os.Build.DEVICE
|
import android.os.Build.DEVICE
|
||||||
|
@ -52,8 +53,6 @@ import ani.dantotsu.databinding.ActivitySettingsMangaBinding
|
||||||
import ani.dantotsu.databinding.ActivitySettingsNotificationsBinding
|
import ani.dantotsu.databinding.ActivitySettingsNotificationsBinding
|
||||||
import ani.dantotsu.databinding.ActivitySettingsThemeBinding
|
import ani.dantotsu.databinding.ActivitySettingsThemeBinding
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
|
||||||
import ani.dantotsu.downloadsPermission
|
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.MediaType
|
import ani.dantotsu.media.MediaType
|
||||||
|
@ -82,7 +81,10 @@ import ani.dantotsu.startMainActivity
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
|
import ani.dantotsu.util.LauncherWrapper
|
||||||
import ani.dantotsu.util.Logger
|
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.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import eltos.simpledialogfragment.SimpleDialog
|
import eltos.simpledialogfragment.SimpleDialog
|
||||||
|
@ -91,7 +93,9 @@ import eltos.simpledialogfragment.color.SimpleColorDialog
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -104,6 +108,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
|
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
|
||||||
}
|
}
|
||||||
lateinit var binding: ActivitySettingsBinding
|
lateinit var binding: ActivitySettingsBinding
|
||||||
|
lateinit var launcher: LauncherWrapper
|
||||||
private lateinit var bindingAccounts: ActivitySettingsAccountsBinding
|
private lateinit var bindingAccounts: ActivitySettingsAccountsBinding
|
||||||
private lateinit var bindingTheme: ActivitySettingsThemeBinding
|
private lateinit var bindingTheme: ActivitySettingsThemeBinding
|
||||||
private lateinit var bindingExtensions: ActivitySettingsExtensionsBinding
|
private lateinit var bindingExtensions: ActivitySettingsExtensionsBinding
|
||||||
|
@ -115,6 +120,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
||||||
private var cursedCounter = 0
|
private var cursedCounter = 0
|
||||||
|
|
||||||
|
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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.text = getString(R.string.version_current, BuildConfig.VERSION_NAME)
|
||||||
binding.settingsVersion.setOnLongClickListener {
|
binding.settingsVersion.setOnLongClickListener {
|
||||||
|
@ -457,11 +465,6 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||||
downloadsManager.purgeDownloads(MediaType.ANIME)
|
downloadsManager.purgeDownloads(MediaType.ANIME)
|
||||||
DownloadService.sendRemoveAllDownloads(
|
|
||||||
this@SettingsActivity,
|
|
||||||
ExoplayerDownloadService::class.java,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.no) { dialog, _ ->
|
.setNegativeButton(R.string.no) { dialog, _ ->
|
||||||
|
@ -724,20 +727,6 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
restartApp(binding.root)
|
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.isChecked = PrefManager.getVal(PrefName.ContinueMedia)
|
||||||
settingsContinueMedia.setOnCheckedChangeListener { _, isChecked ->
|
settingsContinueMedia.setOnCheckedChangeListener { _, isChecked ->
|
||||||
PrefManager.setVal(PrefName.ContinueMedia, isChecked)
|
PrefManager.setVal(PrefName.ContinueMedia, isChecked)
|
||||||
|
@ -757,6 +746,44 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
PrefManager.setVal(PrefName.AdultOnly, isChecked)
|
PrefManager.setVal(PrefName.AdultOnly, isChecked)
|
||||||
restartApp(binding.root)
|
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)) {
|
var previousStart: View = when (PrefManager.getVal<Int>(PrefName.DefaultStartUpTab)) {
|
||||||
0 -> uiSettingsAnime
|
0 -> uiSettingsAnime
|
||||||
1 -> uiSettingsHome
|
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)),
|
OfflineView(Pref(Location.General, Int::class, 0)),
|
||||||
DownloadManager(Pref(Location.General, Int::class, 0)),
|
DownloadManager(Pref(Location.General, Int::class, 0)),
|
||||||
NSFWExtension(Pref(Location.General, Boolean::class, true)),
|
NSFWExtension(Pref(Location.General, Boolean::class, true)),
|
||||||
SdDl(Pref(Location.General, Boolean::class, false)),
|
|
||||||
ContinueMedia(Pref(Location.General, Boolean::class, true)),
|
ContinueMedia(Pref(Location.General, Boolean::class, true)),
|
||||||
SearchSources(Pref(Location.General, Boolean::class, true)),
|
SearchSources(Pref(Location.General, Boolean::class, true)),
|
||||||
RecentlyListOnly(Pref(Location.General, Boolean::class, false)),
|
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)),
|
RecentGlobalNotification(Pref(Location.Irrelevant, Int::class, 0)),
|
||||||
CommentNotificationStore(Pref(Location.Irrelevant, List::class, listOf<CommentStore>())),
|
CommentNotificationStore(Pref(Location.Irrelevant, List::class, listOf<CommentStore>())),
|
||||||
UnreadCommentNotifications(Pref(Location.Irrelevant, Int::class, 0)),
|
UnreadCommentNotifications(Pref(Location.Irrelevant, Int::class, 0)),
|
||||||
|
DownloadsDir(Pref(Location.Irrelevant, String::class, "")),
|
||||||
|
|
||||||
//Protected
|
//Protected
|
||||||
DiscordToken(Pref(Location.Protected, String::class, "")),
|
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.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
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.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
@ -69,7 +70,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
*/
|
*/
|
||||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||||
add("User-Agent", network.defaultUserAgentProvider())
|
add("User-Agent", defaultUserAgentProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -89,5 +89,7 @@ class NetworkHelper(
|
||||||
responseParser = Mapper
|
responseParser = Mapper
|
||||||
)
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
fun defaultUserAgentProvider() = PrefManager.getVal<String>(PrefName.DefaultUserAgent)
|
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.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
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.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
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.
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
*/
|
*/
|
||||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
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:layout_marginBottom="16dp"
|
||||||
android:background="?android:attr/listDivider" />
|
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
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -317,5 +296,29 @@
|
||||||
app:drawableTint="?attr/colorPrimary"
|
app:drawableTint="?attr/colorPrimary"
|
||||||
app:showText="false"
|
app:showText="false"
|
||||||
app:thumbTint="@color/button_switch_track" />
|
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>
|
</ani.dantotsu.others.Xpandable>
|
||||||
</merge>
|
</merge>
|
|
@ -748,7 +748,9 @@
|
||||||
<string name="follows_you">Follows you</string>
|
<string name="follows_you">Follows you</string>
|
||||||
<string name="mutual">Mutual</string>
|
<string name="mutual">Mutual</string>
|
||||||
<string name="success">Success</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="upcoming">Upcoming</string>
|
||||||
<string name="no_shows_to_display">No shows to display</string>
|
<string name="no_shows_to_display">No shows to display</string>
|
||||||
<string name="extension_name">Extension Name</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="trending_manhwa">Trending Manhwa</string>
|
||||||
<string name="liked_by">Liked By</string>
|
<string name="liked_by">Liked By</string>
|
||||||
<string name="adult_only_content">Adult only content</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="report">Report</string>
|
||||||
<string name="ban">Ban</string>
|
<string name="ban">Ban</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue