mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-13 13:47:40 +00:00
Compare commits
10 Commits
l10n_dev_c
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
870cb751a4 | ||
|
|
4ffe9d7505 | ||
|
|
513b937e59 | ||
|
|
6113a10556 | ||
|
|
233f4bfb48 | ||
|
|
3fd01d582a | ||
|
|
00758af458 | ||
|
|
4477e3a0e1 | ||
|
|
e475cc5c01 | ||
|
|
3622d91886 |
@@ -636,6 +636,23 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
fun ImageView.loadImage(file: FileUrl?, width: Int = 0, height: Int = 0) {
|
||||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||
if (file?.url?.isNotEmpty() == true) {
|
||||
tryWith {
|
||||
if (file.url.startsWith("content://")) {
|
||||
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
|
||||
.override(width, height).into(this)
|
||||
} else {
|
||||
val glideUrl = GlideUrl(file.url) { file.headers }
|
||||
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(width, height)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
||||
if (file?.exists() == true) {
|
||||
tryWith {
|
||||
|
||||
@@ -40,6 +40,7 @@ import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.DialogUserAgentBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
@@ -493,16 +494,14 @@ class MainActivity : AppCompatActivity() {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView =
|
||||
LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null)
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
dialogView.userAgentTextBox.hint = "Password"
|
||||
dialogView.subtitle.visibility = View.VISIBLE
|
||||
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setView(dialogView)
|
||||
.setView(dialogView.root)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
password.fill('0')
|
||||
|
||||
@@ -79,7 +79,6 @@ class AddonDownloader {
|
||||
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val installerSteps = InstallerSteps(notificationManager, activity)
|
||||
manager.install(this)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package ani.dantotsu.addons.download
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
@@ -76,7 +75,6 @@ internal class AddonInstallReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader
|
||||
import ani.dantotsu.addons.AddonInstallReceiver
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
|
||||
@@ -10,7 +10,7 @@ import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.addons.download.AddonInstallReceiver
|
||||
import ani.dantotsu.addons.AddonInstallReceiver
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
|
||||
381
app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
Normal file
381
app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
Normal file
@@ -0,0 +1,381 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.anime.OfflineAnimeModel
|
||||
import ani.dantotsu.download.manga.OfflineMangaModel
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.Episode
|
||||
import ani.dantotsu.parsers.MangaChapter
|
||||
import ani.dantotsu.parsers.MangaImage
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
class DownloadCompat {
|
||||
companion object {
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun loadMediaCompat(downloadedType: DownloadedType): Media? {
|
||||
val type = when (downloadedType.type) {
|
||||
MediaType.MANGA -> "Manga"
|
||||
MediaType.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.titleName}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
return try {
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
|
||||
SAnimeImpl() // Provide an instance of SAnimeImpl
|
||||
})
|
||||
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
|
||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun loadOfflineAnimeModelCompat(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = when (downloadedType.type) {
|
||||
MediaType.MANGA -> "Manga"
|
||||
MediaType.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.titleName}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val mediaModel = loadMediaCompat(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalEpisode =
|
||||
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
|
||||
?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
|
||||
val chapters = " Chapters"
|
||||
val totalEpisodesList =
|
||||
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
|
||||
?: "~").toString()
|
||||
return OfflineAnimeModel(
|
||||
title,
|
||||
score,
|
||||
totalEpisode,
|
||||
totalEpisodesList,
|
||||
watchedEpisodes,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
isUserScored,
|
||||
coverUri,
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun loadOfflineMangaModelCompat(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = when (downloadedType.type) {
|
||||
MediaType.MANGA -> "Manga"
|
||||
MediaType.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.titleName}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val mediaModel = loadMediaCompat(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val chapters = " Chapters"
|
||||
return OfflineMangaModel(
|
||||
title,
|
||||
score,
|
||||
totalchapter,
|
||||
readchapter,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
isUserScored,
|
||||
coverUri,
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
suspend fun loadEpisodesCompat(
|
||||
animeLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sAnime: SAnime
|
||||
): List<Episode> {
|
||||
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${animeLocation}/$animeLink"
|
||||
)
|
||||
//get all of the folder names and add them to the list
|
||||
val episodes = mutableListOf<Episode>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
//put the title and episdode number in the extra data
|
||||
val extraData = mutableMapOf<String, String>()
|
||||
extraData["title"] = animeLink
|
||||
extraData["episode"] = it.name
|
||||
if (it.isDirectory) {
|
||||
val episode = Episode(
|
||||
it.name,
|
||||
"$animeLink - ${it.name}",
|
||||
it.name,
|
||||
null,
|
||||
null,
|
||||
extra = extraData,
|
||||
sEpisode = SEpisodeImpl()
|
||||
)
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
|
||||
return episodes
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
suspend fun loadChaptersCompat(
|
||||
mangaLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sManga: SManga
|
||||
): List<MangaChapter> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$mangaLink"
|
||||
)
|
||||
//get all of the folder names and add them to the list
|
||||
val chapters = mutableListOf<MangaChapter>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (it.isDirectory) {
|
||||
val chapter = MangaChapter(
|
||||
it.name,
|
||||
"$mangaLink/${it.name}",
|
||||
it.name,
|
||||
null,
|
||||
null,
|
||||
SChapter.create()
|
||||
)
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
|
||||
return chapters
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
suspend fun loadImagesCompat(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$chapterLink"
|
||||
)
|
||||
val images = mutableListOf<MangaImage>()
|
||||
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (it.isFile) {
|
||||
val image = MangaImage(it.absolutePath, false, null)
|
||||
images.add(image)
|
||||
}
|
||||
}
|
||||
images.sortBy { image ->
|
||||
val matchResult = imageNumberRegex.find(image.url.url)
|
||||
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
|
||||
}
|
||||
for (image in images) {
|
||||
Logger.log("imageNumber: ${image.url.url}")
|
||||
}
|
||||
return images
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun loadSubtitleCompat(title: String, episode: String): List<Subtitle>? {
|
||||
currContext()?.let {
|
||||
File(
|
||||
it.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title/$episode"
|
||||
).listFiles()?.forEach {
|
||||
if (it.name.contains("subtitle")) {
|
||||
return listOf(
|
||||
Subtitle(
|
||||
"Downloaded Subtitle",
|
||||
Uri.fromFile(it).toString(),
|
||||
determineSubtitletype(it.absolutePath)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun determineSubtitletype(url: String): SubtitleType {
|
||||
return when {
|
||||
url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
|
||||
url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
|
||||
else -> SubtitleType.SRT
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun removeMediaCompat(context: Context, title: String, type: MediaType) {
|
||||
val subDirectory = if (type == MediaType.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == MediaType.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory/$title"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
directory.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("external storage is deprecated, use SAF instead")
|
||||
fun removeDownloadCompat(context: Context, downloadedType: DownloadedType) {
|
||||
val directory = if (downloadedType.type == MediaType.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||
)
|
||||
} else if (downloadedType.type == MediaType.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.titleName}/${downloadedType.chapterName}"
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val animeLocation = "Dantotsu/Anime"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package ani.dantotsu.download
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.removeDownloadCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.removeMediaCompat
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
@@ -58,6 +60,7 @@ class DownloadsManager(private val context: Context) {
|
||||
toast: Boolean = true,
|
||||
onFinished: () -> Unit
|
||||
) {
|
||||
removeDownloadCompat(context, downloadedType)
|
||||
downloadsList.remove(downloadedType)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
removeDirectory(downloadedType, toast)
|
||||
@@ -69,6 +72,7 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: MediaType) {
|
||||
removeMediaCompat(context, title, type)
|
||||
val baseDirectory = getBaseDirectory(context, type)
|
||||
val directory = baseDirectory?.findFolder(title)
|
||||
if (directory?.exists() == true) {
|
||||
@@ -84,15 +88,15 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
when (type) {
|
||||
MediaType.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA }
|
||||
}
|
||||
|
||||
MediaType.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME }
|
||||
}
|
||||
|
||||
MediaType.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
|
||||
downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL }
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
@@ -115,7 +119,7 @@ class DownloadsManager(private val context: Context) {
|
||||
if (directory?.exists() == true && directory.isDirectory) {
|
||||
val files = directory.listFiles()
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
if (!downloadsSubLists.any { it.titleName == file.name }) {
|
||||
file.deleteRecursively(context, false)
|
||||
}
|
||||
}
|
||||
@@ -124,8 +128,8 @@ class DownloadsManager(private val context: Context) {
|
||||
val iterator = downloadsList.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val download = iterator.next()
|
||||
val downloadDir = directory?.findFolder(download.title)
|
||||
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
|
||||
val downloadDir = directory?.findFolder(download.titleName)
|
||||
if ((downloadDir?.exists() == false && download.type == type) || download.titleName.isBlank()) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
@@ -211,16 +215,17 @@ class DownloadsManager(private val context: Context) {
|
||||
|
||||
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
|
||||
return if (type == null) {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||
downloadsList.any { it.titleName == title && it.chapterName == chapter }
|
||||
} else {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
|
||||
downloadsList.any { it.titleName == title && it.chapterName == chapter && it.type == type }
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
|
||||
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
||||
val directory =
|
||||
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
|
||||
baseDirectory?.findFolder(downloadedType.titleName)
|
||||
?.findFolder(downloadedType.chapterName)
|
||||
downloadsList.remove(downloadedType)
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory?.exists() == true) {
|
||||
@@ -364,15 +369,21 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
|
||||
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
|
||||
private fun String?.findValidName(): String {
|
||||
fun String?.findValidName(): String {
|
||||
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||
}
|
||||
|
||||
data class DownloadedType(
|
||||
val pTitle: String, val pChapter: String, val type: MediaType
|
||||
private val pTitle: String?,
|
||||
private val pChapter: String?,
|
||||
val type: MediaType,
|
||||
@Deprecated("use pTitle instead")
|
||||
private val title: String? = null,
|
||||
@Deprecated("use pChapter instead")
|
||||
private val chapter: String? = null
|
||||
) : Serializable {
|
||||
val title: String
|
||||
get() = pTitle.findValidName()
|
||||
val chapter: String
|
||||
get() = pChapter.findValidName()
|
||||
val titleName: String
|
||||
get() = title?:pTitle.findValidName()
|
||||
val chapterName: String
|
||||
get() = chapter?:pChapter.findValidName()
|
||||
}
|
||||
|
||||
@@ -31,9 +31,12 @@ import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.findValidName
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
@@ -175,7 +178,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val media =
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
media?.let {
|
||||
lifecycleScope.launch {
|
||||
val mediaModel = getMedia(it)
|
||||
@@ -287,10 +290,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
}
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
@@ -313,7 +316,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
return try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
@@ -327,7 +330,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
})
|
||||
.create()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return null
|
||||
?: return loadMediaCompat(downloadedType)
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
@@ -352,7 +355,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
@@ -363,6 +366,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||
banner.uri
|
||||
} else null
|
||||
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
@@ -391,22 +395,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
return try {
|
||||
loadOfflineAnimeModelCompat(downloadedType)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,13 @@ import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.findValidName
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
@@ -169,8 +172,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val media =
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
media?.let {
|
||||
lifecycleScope.launch {
|
||||
ContextCompat.startActivity(
|
||||
@@ -190,7 +193,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: MediaType =
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) {
|
||||
MediaType.MANGA
|
||||
} else {
|
||||
MediaType.NOVEL
|
||||
@@ -278,19 +281,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
downloads = listOf()
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
@@ -315,7 +318,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
return try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
@@ -323,7 +326,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
})
|
||||
.create()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return null
|
||||
?: return DownloadCompat.loadMediaCompat(downloadedType)
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
@@ -343,7 +346,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
false, downloadedType.titleName
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
@@ -354,6 +357,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||
banner.uri
|
||||
} else null
|
||||
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
?: 0) else mediaModel.userScore) / 10.0).toString()
|
||||
@@ -376,21 +380,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
return try {
|
||||
loadOfflineMangaModelCompat(downloadedType)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,17 @@ import androidx.annotation.OptIn
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.scheduler.Requirements
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
@@ -22,8 +32,12 @@ import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
object Helper {
|
||||
@@ -104,4 +118,92 @@ object Helper {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@UnstableApi
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
fun downloadManager(context: Context): DownloadManager {
|
||||
return download ?: let {
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
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?
|
||||
) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
Logger.log("Download Completed")
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
Logger.log("Download Failed")
|
||||
} else if (download.state == Download.STATE_STOPPED) {
|
||||
Logger.log("Download Stopped")
|
||||
} else if (download.state == Download.STATE_QUEUED) {
|
||||
Logger.log("Download Queued")
|
||||
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||
Logger.log("Download Downloading")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
downloadManager
|
||||
}
|
||||
}
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
@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!!
|
||||
}
|
||||
}
|
||||
@Synchronized
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private fun getDownloadDirectory(context: Context): File {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(null)
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.filesDir
|
||||
}
|
||||
}
|
||||
return downloadDirectory!!
|
||||
}
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var download: DownloadManager? = null
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var simpleCache: SimpleCache? = null
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var downloadDirectory: File? = null
|
||||
}
|
||||
@@ -70,7 +70,7 @@ object MediaNameAdapter {
|
||||
return if (seasonMatcher.find()) {
|
||||
seasonMatcher.group(2)?.toInt()
|
||||
} else {
|
||||
null
|
||||
text.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ object MediaNameAdapter {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
text.toFloatOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ object MediaNameAdapter {
|
||||
if (failedChapterNumberMatcher.find()) {
|
||||
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
text.toFloatOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,14 +305,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
private fun fadeInAnimation(): Animation {
|
||||
return AlphaAnimation(0f, 1f).apply {
|
||||
duration = 150
|
||||
fillAfter = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun fadeOutAnimation(): Animation {
|
||||
return AlphaAnimation(1f, 0f).apply {
|
||||
duration = 150
|
||||
fillAfter = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ class SubtitleDownloader {
|
||||
context,
|
||||
downloadedType.type,
|
||||
false,
|
||||
downloadedType.title,
|
||||
downloadedType.chapter
|
||||
downloadedType.titleName,
|
||||
downloadedType.chapterName
|
||||
) ?: throw Exception("Could not create directory")
|
||||
val type = loadSubtitleType(url)
|
||||
directory.findFile("subtitle.${type}")?.delete()
|
||||
|
||||
@@ -553,8 +553,8 @@ class AnimeWatchFragment : Fragment() {
|
||||
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
|
||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
for (download in downloadManager.animeDownloadedTypes) {
|
||||
if (media.compareName(download.title)) {
|
||||
episodeAdapter.stopDownload(download.chapter)
|
||||
if (media.compareName(download.titleName)) {
|
||||
episodeAdapter.stopDownload(download.chapterName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.getCurrentBrightnessValue
|
||||
import ani.dantotsu.hideSystemBars
|
||||
@@ -1481,26 +1482,38 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
val downloadedMediaItem = if (ext.server.offline) {
|
||||
val titleName = ext.server.name.split("/").first()
|
||||
val episodeName = ext.server.name.split("/").last()
|
||||
downloadId = PrefManager.getAnimeDownloadPreferences()
|
||||
.getString("$titleName - $episodeName", null) ?:
|
||||
PrefManager.getAnimeDownloadPreferences()
|
||||
.getString(ext.server.name, null)
|
||||
val exoItem = if (downloadId != null) {
|
||||
Helper.downloadManager(this)
|
||||
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
|
||||
} else null
|
||||
if (exoItem != null) {
|
||||
exoItem
|
||||
} else {
|
||||
|
||||
val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
|
||||
if (directory != null) {
|
||||
val files = directory.listFiles()
|
||||
println(files)
|
||||
val docFile = directory.listFiles().firstOrNull {
|
||||
it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true
|
||||
}
|
||||
if (docFile != null) {
|
||||
val uri = docFile.uri
|
||||
MediaItem.Builder().setUri(uri).setMimeType(mimeType).build()
|
||||
val directory =
|
||||
getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
|
||||
if (directory != null) {
|
||||
val files = directory.listFiles()
|
||||
println(files)
|
||||
val docFile = directory.listFiles().firstOrNull {
|
||||
it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true
|
||||
}
|
||||
if (docFile != null) {
|
||||
val uri = docFile.uri
|
||||
MediaItem.Builder().setUri(uri).setMimeType(mimeType).build()
|
||||
} else {
|
||||
snackString("File not found")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
snackString("File not found")
|
||||
snackString("Directory not found")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
snackString("Directory not found")
|
||||
null
|
||||
}
|
||||
|
||||
} else null
|
||||
|
||||
mediaItem = if (downloadedMediaItem == null) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.copyToClipboard
|
||||
@@ -48,6 +49,7 @@ import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWith
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -478,6 +480,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
media!!.userPreferredName
|
||||
)
|
||||
} else {
|
||||
val downloadAddonManager: DownloadAddonManager = Injekt.get()
|
||||
if (!downloadAddonManager.isAvailable()){
|
||||
toast("Download Extension not available")
|
||||
return@setSafeOnClickListener
|
||||
}
|
||||
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
||||
val selectedVideo =
|
||||
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||
@@ -488,7 +495,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
||||
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||
if (!torrentExtension.isAvailable()) {
|
||||
snackString("Torrent Extension not available")
|
||||
toast("Torrent Extension not available")
|
||||
return@setSafeOnClickListener
|
||||
}
|
||||
runBlocking {
|
||||
|
||||
@@ -121,7 +121,6 @@ class CommentsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast("Not logged in")
|
||||
activity.binding.commentMessageContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
@@ -194,8 +194,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
)
|
||||
|
||||
for (download in downloadManager.mangaDownloadedTypes) {
|
||||
if (media.compareName(download.title)) {
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
if (media.compareName(download.titleName)) {
|
||||
chapterAdapter.stopDownload(download.chapterName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,9 +117,10 @@ class NovelReadFragment : Fragment(),
|
||||
context ?: currContext()!!,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
media.mainName(),
|
||||
novel.name
|
||||
)
|
||||
val file = directory?.findFile(novel.name)
|
||||
val file = directory?.findFile("0.epub")
|
||||
if (file?.exists() == false) return false
|
||||
val fileUri = file?.uri ?: return false
|
||||
val intent = Intent(context, NovelReaderActivity::class.java).apply {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemNovelResponseBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.snackString
|
||||
@@ -37,10 +38,7 @@ class NovelResponseAdapter(
|
||||
val binding = holder.binding
|
||||
val novel = list[position]
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
|
||||
val cover = GlideUrl(novel.coverUrl.url) { novel.coverUrl.headers }
|
||||
Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0)
|
||||
.into(binding.itemEpisodeImage)
|
||||
binding.itemEpisodeImage.loadImage(novel.coverUrl, 400, 0)
|
||||
|
||||
val typedValue = TypedValue()
|
||||
fragment.requireContext().theme?.resolveAttribute(
|
||||
|
||||
@@ -2,6 +2,8 @@ package ani.dantotsu.parsers
|
||||
|
||||
import android.app.Application
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadEpisodesCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadSubtitleCompat
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
||||
@@ -53,8 +55,11 @@ class OfflineAnimeParser : AnimeParser() {
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
|
||||
return episodes
|
||||
//episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
|
||||
episodes.addAll(loadEpisodesCompat(animeLink, extra, sAnime))
|
||||
//filter those with the same name
|
||||
return episodes.distinctBy { it.number }
|
||||
.sortedBy { MediaNameAdapter.findEpisodeNumber(it.number) }
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
@@ -75,14 +80,16 @@ class OfflineAnimeParser : AnimeParser() {
|
||||
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val returnTitles: MutableList<String> = mutableListOf()
|
||||
val titles = downloadManager.animeDownloadedTypes.map { it.titleName }.distinct()
|
||||
val returnTitlesPair: MutableList<Pair<String, Int>> = mutableListOf()
|
||||
for (title in titles) {
|
||||
Logger.log("Comparing $title to $query")
|
||||
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
|
||||
returnTitles.add(title)
|
||||
val score = FuzzySearch.ratio(title.lowercase(), query.lowercase())
|
||||
if (score > 80) {
|
||||
returnTitlesPair.add(Pair(title, score))
|
||||
}
|
||||
}
|
||||
val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first }
|
||||
val returnList: MutableList<ShowResponse> = mutableListOf()
|
||||
for (title in returnTitles) {
|
||||
returnList.add(ShowResponse(title, title, title))
|
||||
@@ -148,6 +155,7 @@ class OfflineVideoExtractor(private val videoServer: VideoServer) : VideoExtract
|
||||
)
|
||||
}
|
||||
}
|
||||
loadSubtitleCompat(title, episode)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import android.app.Application
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadChaptersCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadImagesCompat
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
@@ -41,8 +43,9 @@ class OfflineMangaParser : MangaParser() {
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
|
||||
return chapters
|
||||
chapters.addAll(loadChaptersCompat(mangaLink, extra, sManga))
|
||||
return chapters.distinctBy { it.number }
|
||||
.sortedBy { MediaNameAdapter.findChapterNumber(it.number) }
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
@@ -60,26 +63,32 @@ class OfflineMangaParser : MangaParser() {
|
||||
images.add(image)
|
||||
}
|
||||
}
|
||||
images.sortBy { image ->
|
||||
val matchResult = imageNumberRegex.find(image.url.url)
|
||||
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
|
||||
}
|
||||
for (image in images) {
|
||||
Logger.log("imageNumber: ${image.url.url}")
|
||||
}
|
||||
return images
|
||||
return if (images.isNotEmpty()) {
|
||||
images.sortBy { image ->
|
||||
val matchResult = imageNumberRegex.find(image.url.url)
|
||||
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
|
||||
}
|
||||
images
|
||||
} else {
|
||||
loadImagesCompat(chapterLink, sChapter)
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val returnTitles: MutableList<String> = mutableListOf()
|
||||
val titles = downloadManager.mangaDownloadedTypes.map { it.titleName }.distinct()
|
||||
val returnTitlesPair: MutableList<Pair<String, Int>> = mutableListOf()
|
||||
for (title in titles) {
|
||||
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
|
||||
returnTitles.add(title)
|
||||
val score = FuzzySearch.ratio(title.lowercase(), query.lowercase())
|
||||
if (score > 80) {
|
||||
returnTitlesPair.add(Pair(title, score))
|
||||
}
|
||||
}
|
||||
val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first }
|
||||
val returnList: MutableList<ShowResponse> = mutableListOf()
|
||||
for (title in returnTitles) {
|
||||
returnList.add(ShowResponse(title, title, title))
|
||||
|
||||
@@ -5,6 +5,7 @@ import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.util.Logger
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -48,13 +49,16 @@ class OfflineNovelParser : NovelParser() {
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val returnTitles: MutableList<String> = mutableListOf()
|
||||
val titles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
|
||||
val returnTitlesPair: MutableList<Pair<String, Int>> = mutableListOf()
|
||||
for (title in titles) {
|
||||
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
|
||||
returnTitles.add(title)
|
||||
Logger.log("Comparing $title to $query")
|
||||
val score = FuzzySearch.ratio(title.lowercase(), query.lowercase())
|
||||
if (score > 80) {
|
||||
returnTitlesPair.add(Pair(title, score))
|
||||
}
|
||||
}
|
||||
val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first }
|
||||
val returnList: MutableList<ShowResponse> = mutableListOf()
|
||||
for (title in returnTitles) {
|
||||
//need to search the subdirectories for the ShowResponses
|
||||
@@ -69,7 +73,7 @@ class OfflineNovelParser : NovelParser() {
|
||||
}
|
||||
val cover = directory?.findFile("cover.jpg")?.uri.toString()
|
||||
names.forEach {
|
||||
returnList.add(ShowResponse(it, it, cover))
|
||||
returnList.add(ShowResponse(it, query, cover))
|
||||
}
|
||||
}
|
||||
return returnList
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package ani.dantotsu.parsers.novel
|
||||
|
||||
import android.os.FileObserver
|
||||
import ani.dantotsu.parsers.novel.FileObserver.fileObserver
|
||||
import ani.dantotsu.util.Logger
|
||||
import java.io.File
|
||||
|
||||
|
||||
class NovelExtensionFileObserver(private val listener: Listener, private val path: String) :
|
||||
FileObserver(path, CREATE or DELETE or MOVED_FROM or MOVED_TO or MODIFY) {
|
||||
|
||||
init {
|
||||
fileObserver = this
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts observing the file changes in the directory.
|
||||
*/
|
||||
fun register() {
|
||||
startWatching()
|
||||
}
|
||||
|
||||
|
||||
override fun onEvent(event: Int, file: String?) {
|
||||
Logger.log("Event: $event")
|
||||
if (file == null) return
|
||||
|
||||
val fullPath = File(path, file)
|
||||
|
||||
when (event) {
|
||||
CREATE -> {
|
||||
Logger.log("File created: $fullPath")
|
||||
listener.onExtensionFileCreated(fullPath)
|
||||
}
|
||||
|
||||
DELETE -> {
|
||||
Logger.log("File deleted: $fullPath")
|
||||
listener.onExtensionFileDeleted(fullPath)
|
||||
}
|
||||
|
||||
MODIFY -> {
|
||||
Logger.log("File modified: $fullPath")
|
||||
listener.onExtensionFileModified(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onExtensionFileCreated(file: File)
|
||||
fun onExtensionFileDeleted(file: File)
|
||||
fun onExtensionFileModified(file: File)
|
||||
}
|
||||
}
|
||||
|
||||
object FileObserver {
|
||||
var fileObserver: FileObserver? = null
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
@@ -87,7 +88,7 @@ class NovelExtensionGithubApi {
|
||||
}
|
||||
}
|
||||
|
||||
val installedExtensions = NovelExtensionLoader.loadExtensions(context)
|
||||
val installedExtensions = ExtensionLoader.loadNovelExtensions(context)
|
||||
.filterIsInstance<AnimeLoadResult.Success>()
|
||||
.map { it.extension }
|
||||
|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
package ani.dantotsu.parsers.novel
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
internal class NovelExtensionInstaller(private val context: Context) {
|
||||
|
||||
/**
|
||||
* The system's download manager
|
||||
*/
|
||||
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
||||
|
||||
/**
|
||||
* The broadcast receiver which listens to download completion events.
|
||||
*/
|
||||
private val downloadReceiver = DownloadCompletionReceiver()
|
||||
|
||||
/**
|
||||
* The currently requested downloads, with the package name (unique id) as key, and the id
|
||||
* returned by the download manager.
|
||||
*/
|
||||
private val activeDownloads = hashMapOf<String, Long>()
|
||||
|
||||
/**
|
||||
* Relay used to notify the installation step of every download.
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
||||
|
||||
/**
|
||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||
* step in the installation process.
|
||||
*
|
||||
* @param url The url of the apk.
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
fun downloadAndInstall(url: String, extension: NovelExtension): Observable<InstallStep> =
|
||||
Observable.defer {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
if (oldDownload != null) {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
|
||||
val sourcePath =
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath
|
||||
//if the file is already downloaded, remove it
|
||||
val fileToDelete = File("$sourcePath/${url.toUri().lastPathSegment}")
|
||||
if (fileToDelete.exists()) {
|
||||
if (fileToDelete.delete()) {
|
||||
Logger.log("APK file deleted successfully.")
|
||||
} else {
|
||||
Logger.log("Failed to delete APK file.")
|
||||
}
|
||||
} else {
|
||||
Logger.log("APK file not found.")
|
||||
}
|
||||
|
||||
// Register the receiver after removing (and unregistering) the previous download
|
||||
downloadReceiver.register()
|
||||
|
||||
val downloadUri = url.toUri()
|
||||
val request = DownloadManager.Request(downloadUri)
|
||||
.setTitle(extension.name)
|
||||
.setMimeType(APK_MIME)
|
||||
.setDestinationInExternalFilesDir(
|
||||
context,
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
downloadUri.lastPathSegment
|
||||
)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
val id = downloadManager.enqueue(request)
|
||||
activeDownloads[pkgName] = id
|
||||
|
||||
downloadsRelay.filter { it.first == id }
|
||||
.map { it.second }
|
||||
// Poll download status
|
||||
.mergeWith(pollStatus(id))
|
||||
// Stop when the application is installed or errors
|
||||
.takeUntil { it.isCompleted() }
|
||||
// Always notify on main thread
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Always remove the download when unsubscribed
|
||||
.doOnUnsubscribe { deleteDownload(pkgName) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that polls the given download id for its status every second, as the
|
||||
* manager doesn't have any notification system. It'll stop once the download finishes.
|
||||
*
|
||||
* @param id The id of the download to poll.
|
||||
*/
|
||||
private fun pollStatus(id: Long): Observable<InstallStep> {
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
|
||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
||||
// Get the current download status
|
||||
.map {
|
||||
downloadManager.query(query).use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
||||
} else {
|
||||
DownloadManager.STATUS_FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore duplicate results
|
||||
.distinctUntilChanged()
|
||||
// Stop polling when the download fails or finishes
|
||||
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
|
||||
// Map to our model
|
||||
.flatMap { status ->
|
||||
when (status) {
|
||||
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
|
||||
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
|
||||
DownloadManager.STATUS_SUCCESSFUL -> Observable.just(InstallStep.Installing)
|
||||
else -> Observable.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun installApk(downloadId: Long, uri: Uri, context: Context, pkgName: String): InstallStep {
|
||||
val sourcePath =
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/" + uri.lastPathSegment
|
||||
val destinationPath =
|
||||
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
|
||||
|
||||
// Check if source path is obtained correctly
|
||||
if (!sourcePath.startsWith(FILE_SCHEME)) {
|
||||
Logger.log("Source APK path not found.")
|
||||
downloadsRelay.call(downloadId to InstallStep.Error)
|
||||
return InstallStep.Error
|
||||
}
|
||||
|
||||
// Create the destination directory if it doesn't exist
|
||||
val destinationDir = File(destinationPath).parentFile
|
||||
if (destinationDir?.exists() == false) {
|
||||
destinationDir.mkdirs()
|
||||
}
|
||||
if (destinationDir?.setWritable(true) == false) {
|
||||
Logger.log("Failed to set destinationDir to writable.")
|
||||
downloadsRelay.call(downloadId to InstallStep.Error)
|
||||
return InstallStep.Error
|
||||
}
|
||||
|
||||
// Copy the file to the new location
|
||||
copyFileToInternalStorage(sourcePath, destinationPath)
|
||||
Logger.log("APK moved to $destinationPath")
|
||||
downloadsRelay.call(downloadId to InstallStep.Installed)
|
||||
return InstallStep.Installed
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels extension install and remove from download manager and installer.
|
||||
*/
|
||||
fun cancelInstall(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||
downloadManager.remove(downloadId)
|
||||
}
|
||||
|
||||
fun uninstallApk(pkgName: String, context: Context) {
|
||||
val apkPath =
|
||||
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
|
||||
val fileToDelete = File(apkPath)
|
||||
//give write permission to the file
|
||||
if (fileToDelete.exists() && !fileToDelete.canWrite()) {
|
||||
Logger.log("File is not writable. Giving write permission.")
|
||||
val a = fileToDelete.setWritable(true)
|
||||
Logger.log("Success: $a")
|
||||
}
|
||||
//set the directory to writable
|
||||
val destinationDir = File(apkPath).parentFile
|
||||
if (destinationDir?.exists() == false) {
|
||||
destinationDir.mkdirs()
|
||||
}
|
||||
val s = destinationDir?.setWritable(true)
|
||||
Logger.log("Success destinationDir: $s")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
Files.delete(fileToDelete.toPath())
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Failed to delete APK file.")
|
||||
Logger.log(e)
|
||||
snackString("Failed to delete APK file.")
|
||||
}
|
||||
} else {
|
||||
if (fileToDelete.exists()) {
|
||||
if (fileToDelete.delete()) {
|
||||
Logger.log("APK file deleted successfully.")
|
||||
snackString("APK file deleted successfully.")
|
||||
} else {
|
||||
Logger.log("Failed to delete APK file.")
|
||||
snackString("Failed to delete APK file.")
|
||||
}
|
||||
} else {
|
||||
Logger.log("APK file not found.")
|
||||
snackString("APK file not found.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFileToInternalStorage(sourcePath: String, destinationPath: String) {
|
||||
val source = File(sourcePath)
|
||||
val destination = File(destinationPath)
|
||||
destination.setWritable(true)
|
||||
|
||||
//delete the file if it already exists
|
||||
if (destination.exists()) {
|
||||
if (destination.delete()) {
|
||||
Logger.log("File deleted successfully.")
|
||||
} else {
|
||||
Logger.log("Failed to delete file.")
|
||||
}
|
||||
}
|
||||
|
||||
var inputChannel: FileChannel? = null
|
||||
var outputChannel: FileChannel? = null
|
||||
try {
|
||||
inputChannel = FileInputStream(source).channel
|
||||
outputChannel = FileOutputStream(destination).channel
|
||||
inputChannel.transferTo(0, inputChannel.size(), outputChannel)
|
||||
destination.setWritable(false)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
inputChannel?.close()
|
||||
outputChannel?.close()
|
||||
}
|
||||
|
||||
Logger.log("File copied to internal storage.")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun getRealPathFromURI(context: Context, contentUri: Uri): String? {
|
||||
var cursor: Cursor? = null
|
||||
try {
|
||||
val proj = arrayOf(MediaStore.Images.Media.DATA)
|
||||
cursor = context.contentResolver.query(contentUri, proj, null, null, null)
|
||||
val columnIndex = cursor?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||
if (cursor != null && cursor.moveToFirst() && columnIndex != null) {
|
||||
return cursor.getString(columnIndex)
|
||||
}
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the step of the installation of an extension.
|
||||
*
|
||||
* @param downloadId The id of the download.
|
||||
* @param step New install step.
|
||||
*/
|
||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||
downloadsRelay.call(downloadId to step)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the download for the given package name.
|
||||
*
|
||||
* @param pkgName The package name of the download to delete.
|
||||
*/
|
||||
private fun deleteDownload(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName)
|
||||
if (downloadId != null) {
|
||||
downloadManager.remove(downloadId)
|
||||
}
|
||||
if (activeDownloads.isEmpty()) {
|
||||
downloadReceiver.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiver that listens to download status events.
|
||||
*/
|
||||
private inner class DownloadCompletionReceiver : BroadcastReceiver() {
|
||||
|
||||
/**
|
||||
* Whether this receiver is currently registered.
|
||||
*/
|
||||
private var isRegistered = false
|
||||
|
||||
/**
|
||||
* Registers this receiver if it's not already.
|
||||
*/
|
||||
fun register() {
|
||||
if (isRegistered) return
|
||||
isRegistered = true
|
||||
|
||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this receiver if it's not already.
|
||||
*/
|
||||
fun unregister() {
|
||||
if (!isRegistered) return
|
||||
isRegistered = false
|
||||
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a download event is received. It looks for the download in the current active
|
||||
* downloads and notifies its installation step.
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
|
||||
|
||||
// Avoid events for downloads we didn't request
|
||||
if (id !in activeDownloads.values) return
|
||||
|
||||
val uri = downloadManager.getUriForDownloadedFile(id)
|
||||
|
||||
// Set next installation step
|
||||
if (uri == null) {
|
||||
Logger.log("Couldn't locate downloaded APK")
|
||||
downloadsRelay.call(id to InstallStep.Error)
|
||||
return
|
||||
}
|
||||
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
downloadManager.query(query).use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val localUri = cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
|
||||
).removePrefix(FILE_SCHEME)
|
||||
val pkgName = extractPkgNameFromUri(localUri)
|
||||
installApk(id, File(localUri).getUriCompat(context), context, pkgName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractPkgNameFromUri(localUri: String): String {
|
||||
val uri = Uri.parse(localUri)
|
||||
val path = uri.path
|
||||
val pkgName = path?.substring(path.lastIndexOf('/') + 1)?.removeSuffix(".apk")
|
||||
Logger.log("Package name: $pkgName")
|
||||
return pkgName ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val APK_MIME = "application/vnd.android.package-archive"
|
||||
const val FILE_SCHEME = "file://"
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package ani.dantotsu.parsers.novel
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager.GET_SIGNATURES
|
||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||
import android.os.Build
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.parsers.NovelInterface
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
internal object NovelExtensionLoader {
|
||||
|
||||
private const val officialSignature =
|
||||
"a3061edb369278749b8e8de810d440d38e96417bbd67bbdfc5d9d9ed475ce4a5" //dan's key
|
||||
|
||||
fun loadExtensions(context: Context): List<NovelLoadResult> {
|
||||
val installDir = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/"
|
||||
val results = mutableListOf<NovelLoadResult>()
|
||||
//the number of files
|
||||
Logger.log("Loading extensions from $installDir")
|
||||
Logger.log(
|
||||
"Loading extensions from ${File(installDir).listFiles()?.size}"
|
||||
)
|
||||
File(installDir).setWritable(false)
|
||||
File(installDir).listFiles()?.forEach {
|
||||
//set the file to read only
|
||||
it.setWritable(false)
|
||||
Logger.log("Loading extension ${it.name}")
|
||||
val extension = loadExtension(context, it)
|
||||
if (extension is NovelLoadResult.Success) {
|
||||
results.add(extension)
|
||||
} else {
|
||||
Logger.log("Failed to load extension ${it.name}")
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load an extension from the given package name. It checks if the extension
|
||||
* contains the required feature flag before trying to load it.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun loadExtensionFromPkgName(context: Context, pkgName: String): NovelLoadResult {
|
||||
val path =
|
||||
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
|
||||
//make /extensions/novel read only
|
||||
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/".let {
|
||||
File(it).setWritable(false)
|
||||
File(it).setReadable(true)
|
||||
}
|
||||
try {
|
||||
context.packageManager.getPackageArchiveInfo(path, 0)
|
||||
} catch (error: Exception) {
|
||||
// Unlikely, but the package may have been uninstalled at this point
|
||||
Logger.log("Failed to load extension $pkgName")
|
||||
return NovelLoadResult.Error(Exception("Failed to load extension"))
|
||||
}
|
||||
return loadExtension(context, File(path))
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun loadExtension(context: Context, file: File): NovelLoadResult {
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
context.packageManager.getPackageArchiveInfo(
|
||||
file.absolutePath,
|
||||
GET_SIGNATURES or GET_SIGNING_CERTIFICATES
|
||||
)
|
||||
?: return NovelLoadResult.Error(Exception("Failed to load extension"))
|
||||
} else {
|
||||
context.packageManager.getPackageArchiveInfo(file.absolutePath, GET_SIGNATURES)
|
||||
?: return NovelLoadResult.Error(Exception("Failed to load extension"))
|
||||
}
|
||||
val appInfo = packageInfo.applicationInfo
|
||||
?: return NovelLoadResult.Error(Exception("Failed to load Extension Info"))
|
||||
appInfo.sourceDir = file.absolutePath
|
||||
appInfo.publicSourceDir = file.absolutePath
|
||||
|
||||
val signatureHash = getSignatureHash(packageInfo)
|
||||
|
||||
if ((signatureHash == null) || !signatureHash.contains(officialSignature)) {
|
||||
Logger.log("Package ${packageInfo.packageName} isn't signed")
|
||||
Logger.log("signatureHash: $signatureHash")
|
||||
snackString("Package ${packageInfo.packageName} isn't signed")
|
||||
//return NovelLoadResult.Error(Exception("Extension not signed"))
|
||||
}
|
||||
|
||||
val extension = NovelExtension.Installed(
|
||||
packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString()
|
||||
?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")),
|
||||
packageInfo.packageName
|
||||
?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")),
|
||||
packageInfo.versionName ?: "",
|
||||
packageInfo.versionCode.toLong(),
|
||||
loadSources(
|
||||
context, file,
|
||||
packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString()!!
|
||||
),
|
||||
packageInfo.applicationInfo?.loadIcon(context.packageManager)
|
||||
)
|
||||
|
||||
return NovelLoadResult.Success(extension)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getSignatureHash(pkgInfo: PackageInfo): List<String>? {
|
||||
val signatures =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) {
|
||||
pkgInfo.signingInfo.apkContentsSigners
|
||||
} else {
|
||||
pkgInfo.signatures
|
||||
}
|
||||
return if (!signatures.isNullOrEmpty()) {
|
||||
signatures.map { Hash.sha256(it.toByteArray()) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSources(context: Context, file: File, className: String): List<NovelInterface> {
|
||||
return try {
|
||||
Logger.log("isFileWritable: ${file.canWrite()}")
|
||||
if (file.canWrite()) {
|
||||
val a = file.setWritable(false)
|
||||
Logger.log("success: $a")
|
||||
}
|
||||
Logger.log("isFileWritable: ${file.canWrite()}")
|
||||
val classLoader = PathClassLoader(file.absolutePath, null, context.classLoader)
|
||||
val extensionClassName =
|
||||
"some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className"
|
||||
val loadedClass = classLoader.loadClass(extensionClassName)
|
||||
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||
val novelInterfaceInstance = instance as? NovelInterface
|
||||
listOfNotNull(novelInterfaceInstance)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class NovelLoadResult {
|
||||
data class Success(val extension: NovelExtension.Installed) : NovelLoadResult()
|
||||
data class Error(val error: Exception) : NovelLoadResult()
|
||||
}
|
||||
@@ -2,14 +2,17 @@ package ani.dantotsu.parsers.novel
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import rx.Observable
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import java.io.File
|
||||
|
||||
class NovelExtensionManager(private val context: Context) {
|
||||
var isInitialized = false
|
||||
@@ -24,7 +27,7 @@ class NovelExtensionManager(private val context: Context) {
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the Novel extensions.
|
||||
*/
|
||||
private val installer by lazy { NovelExtensionInstaller(context) }
|
||||
private val installer by lazy { ExtensionInstaller(context) }
|
||||
|
||||
private val iconMap = mutableMapOf<String, Drawable>()
|
||||
|
||||
@@ -49,12 +52,11 @@ class NovelExtensionManager(private val context: Context) {
|
||||
|
||||
init {
|
||||
initNovelExtensions()
|
||||
val path = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/"
|
||||
NovelExtensionFileObserver(NovelInstallationListener(), path).register()
|
||||
ExtensionInstallReceiver().setNovelListener(NovelInstallationListener()).register(context)
|
||||
}
|
||||
|
||||
private fun initNovelExtensions() {
|
||||
val novelExtensions = NovelExtensionLoader.loadExtensions(context)
|
||||
val novelExtensions = ExtensionLoader.loadNovelExtensions(context)
|
||||
|
||||
_installedNovelExtensionsFlow.value = novelExtensions
|
||||
.filterIsInstance<NovelLoadResult.Success>()
|
||||
@@ -117,7 +119,8 @@ class NovelExtensionManager(private val context: Context) {
|
||||
* @param extension The anime extension to be installed.
|
||||
*/
|
||||
fun installExtension(extension: NovelExtension.Available): Observable<InstallStep> {
|
||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension.pkgName,
|
||||
extension.name, MediaType.NOVEL)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,7 +160,7 @@ class NovelExtensionManager(private val context: Context) {
|
||||
* @param pkgName The package name of the application to uninstall.
|
||||
*/
|
||||
fun uninstallExtension(pkgName: String, context: Context) {
|
||||
installer.uninstallApk(pkgName, context)
|
||||
installer.uninstallApk(pkgName)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,28 +205,18 @@ class NovelExtensionManager(private val context: Context) {
|
||||
/**
|
||||
* Listener which receives events of the novel extensions being installed, updated or removed.
|
||||
*/
|
||||
private inner class NovelInstallationListener : NovelExtensionFileObserver.Listener {
|
||||
|
||||
override fun onExtensionFileCreated(file: File) {
|
||||
NovelExtensionLoader.loadExtension(context, file).let {
|
||||
if (it is NovelLoadResult.Success) {
|
||||
registerNewExtension(it.extension.withUpdateCheck())
|
||||
}
|
||||
}
|
||||
private inner class NovelInstallationListener : ExtensionInstallReceiver.NovelListener {
|
||||
override fun onExtensionInstalled(extension: NovelExtension.Installed) {
|
||||
registerNewExtension(extension.withUpdateCheck())
|
||||
}
|
||||
|
||||
override fun onExtensionFileDeleted(file: File) {
|
||||
val pkgName = file.nameWithoutExtension
|
||||
override fun onExtensionUpdated(extension: NovelExtension.Installed) {
|
||||
registerUpdatedExtension(extension.withUpdateCheck())
|
||||
}
|
||||
|
||||
override fun onPackageUninstalled(pkgName: String) {
|
||||
unregisterNovelExtension(pkgName)
|
||||
}
|
||||
|
||||
override fun onExtensionFileModified(file: File) {
|
||||
NovelExtensionLoader.loadExtension(context, file).let {
|
||||
if (it is NovelLoadResult.Success) {
|
||||
registerUpdatedExtension(it.extension.withUpdateCheck())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package ani.dantotsu.parsers.novel
|
||||
|
||||
|
||||
sealed class NovelLoadResult {
|
||||
data class Success(val extension: NovelExtension.Installed) : NovelLoadResult()
|
||||
data class Error(val error: Exception) : NovelLoadResult()
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class SettingsAddonActivity : AppCompatActivity() {
|
||||
type = 1,
|
||||
name = getString(R.string.anime_downloader_addon),
|
||||
desc = getString(R.string.not_installed),
|
||||
icon = R.drawable.anim_play_to_pause,
|
||||
icon = R.drawable.ic_download_24,
|
||||
isActivity = true,
|
||||
attach = {
|
||||
setStatus(
|
||||
@@ -85,7 +85,7 @@ class SettingsAddonActivity : AppCompatActivity() {
|
||||
it.settingsIconRight.setOnClickListener { _ ->
|
||||
if (it.settingsDesc.text == getString(R.string.installed)) {
|
||||
downloadAddonManager.uninstall()
|
||||
return@setOnClickListener //uninstall logic here
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
job = Job()
|
||||
val scope = CoroutineScope(Dispatchers.Main + job)
|
||||
@@ -118,7 +118,7 @@ class SettingsAddonActivity : AppCompatActivity() {
|
||||
type = 1,
|
||||
name = getString(R.string.torrent_addon),
|
||||
desc = getString(R.string.not_installed),
|
||||
icon = R.drawable.anim_play_to_pause,
|
||||
icon = R.drawable.ic_round_magnet_24,
|
||||
isActivity = true,
|
||||
attach = {
|
||||
setStatus(
|
||||
@@ -176,15 +176,21 @@ class SettingsAddonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.enable_torrent),
|
||||
desc = getString(R.string.enable_torrent),
|
||||
desc = getString(R.string.enable_torrent_desc),
|
||||
icon = R.drawable.ic_round_dns_24,
|
||||
isChecked = PrefManager.getVal(PrefName.TorrentEnabled),
|
||||
switch = { isChecked, _ ->
|
||||
switch = { isChecked, it ->
|
||||
if (isChecked && !torrentAddonManager.isAvailable()) {
|
||||
snackString(getString(R.string.install_torrent_addon))
|
||||
it.settingsButton.isChecked = false
|
||||
PrefManager.setVal(PrefName.TorrentEnabled, false)
|
||||
return@Settings
|
||||
}
|
||||
PrefManager.setVal(PrefName.TorrentEnabled, isChecked)
|
||||
Injekt.get<TorrentAddonManager>().extension?.let {
|
||||
if (isChecked) {
|
||||
lifecycleScope.launchIO {
|
||||
if (!ServerService.isRunning() && torrentAddonManager.isAvailable()) {
|
||||
if (!ServerService.isRunning()) {
|
||||
ServerService.start()
|
||||
}
|
||||
}
|
||||
@@ -196,14 +202,13 @@ class SettingsAddonActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
binding.settingsRecyclerView.layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class SettingsAnimeActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.player_settings),
|
||||
desc = getString(R.string.player_settings),
|
||||
desc = getString(R.string.player_settings_desc),
|
||||
icon = R.drawable.ic_round_video_settings_24,
|
||||
onClick = {
|
||||
startActivity(Intent(context, PlayerSettingsActivity::class.java))
|
||||
@@ -54,7 +54,7 @@ class SettingsAnimeActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.purge_anime_downloads),
|
||||
desc = getString(R.string.purge_anime_downloads),
|
||||
desc = getString(R.string.purge_anime_downloads_desc),
|
||||
icon = R.drawable.ic_round_delete_24,
|
||||
onClick = {
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
@@ -80,7 +80,7 @@ class SettingsAnimeActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.prefer_dub),
|
||||
desc = getString(R.string.prefer_dub),
|
||||
desc = getString(R.string.prefer_dub_desc),
|
||||
icon = R.drawable.ic_round_audiotrack_24,
|
||||
isChecked = PrefManager.getVal(PrefName.SettingsPreferDub),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -90,7 +90,7 @@ class SettingsAnimeActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.show_yt),
|
||||
desc = getString(R.string.show_yt),
|
||||
desc = getString(R.string.show_yt_desc),
|
||||
icon = R.drawable.ic_round_play_circle_24,
|
||||
isChecked = PrefManager.getVal(PrefName.ShowYtButton),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -100,7 +100,7 @@ class SettingsAnimeActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.include_list),
|
||||
desc = getString(R.string.include_list),
|
||||
desc = getString(R.string.include_list_anime_desc),
|
||||
icon = R.drawable.view_list_24,
|
||||
isChecked = PrefManager.getVal(PrefName.IncludeAnimeList),
|
||||
switch = { isChecked, _ ->
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.ActivitySettingsCommonBinding
|
||||
import ani.dantotsu.databinding.DialogUserAgentBinding
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
@@ -33,7 +34,6 @@ import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.LauncherWrapper
|
||||
import ani.dantotsu.util.StoragePermissions
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -136,7 +136,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.ui_settings),
|
||||
desc = getString(R.string.ui_settings),
|
||||
desc = getString(R.string.ui_settings_desc),
|
||||
icon = R.drawable.ic_round_auto_awesome_24,
|
||||
onClick = {
|
||||
startActivity(
|
||||
@@ -151,7 +151,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.download_manager_select),
|
||||
desc = getString(R.string.download_manager_select),
|
||||
desc = getString(R.string.download_manager_select_desc),
|
||||
icon = R.drawable.ic_download_24,
|
||||
onClick = {
|
||||
val managers = arrayOf("Default", "1DM", "ADM")
|
||||
@@ -172,7 +172,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.backup_restore),
|
||||
desc = getString(R.string.backup_restore),
|
||||
desc = getString(R.string.backup_restore_desc),
|
||||
icon = R.drawable.backup_restore,
|
||||
onClick = {
|
||||
StoragePermissions.downloadsPermission(context)
|
||||
@@ -227,7 +227,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.change_download_location),
|
||||
desc = getString(R.string.change_download_location),
|
||||
desc = getString(R.string.change_download_location_desc),
|
||||
icon = R.drawable.ic_round_source_24,
|
||||
onClick = {
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
@@ -267,7 +267,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.always_continue_content),
|
||||
desc = getString(R.string.always_continue_content),
|
||||
desc = getString(R.string.always_continue_content_desc),
|
||||
icon = R.drawable.ic_round_delete_24,
|
||||
isChecked = PrefManager.getVal(PrefName.ContinueMedia),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -277,7 +277,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.search_source_list),
|
||||
desc = getString(R.string.search_source_list),
|
||||
desc = getString(R.string.search_source_list_desc),
|
||||
icon = R.drawable.ic_round_search_sources_24,
|
||||
isChecked = PrefManager.getVal(PrefName.SearchSources),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -287,7 +287,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.recentlyListOnly),
|
||||
desc = getString(R.string.recentlyListOnly),
|
||||
desc = getString(R.string.recentlyListOnly_desc),
|
||||
icon = R.drawable.ic_round_new_releases_24,
|
||||
isChecked = PrefManager.getVal(PrefName.RecentlyListOnly),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -297,7 +297,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.adult_only_content),
|
||||
desc = getString(R.string.adult_only_content),
|
||||
desc = getString(R.string.adult_only_content_desc),
|
||||
icon = R.drawable.ic_round_nsfw_24,
|
||||
isChecked = PrefManager.getVal(PrefName.AdultOnly),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -347,14 +347,15 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null)
|
||||
val box = dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)
|
||||
box?.hint = getString(R.string.password)
|
||||
box?.setSingleLine()
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
val box = dialogView.userAgentTextBox
|
||||
box.hint = getString(R.string.password)
|
||||
box.setSingleLine()
|
||||
|
||||
val dialog =
|
||||
AlertDialog.Builder(this, R.style.MyPopup).setTitle(getString(R.string.enter_password))
|
||||
.setView(dialogView).setPositiveButton(R.string.ok, null)
|
||||
.setView(dialogView.root)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
password.fill('0')
|
||||
dialog.dismiss()
|
||||
@@ -362,8 +363,8 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
}.create()
|
||||
|
||||
fun handleOkAction() {
|
||||
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
|
||||
if (editText?.text?.isNotBlank() == true) {
|
||||
val editText = dialogView.userAgentTextBox
|
||||
if (editText.text?.isNotBlank() == true) {
|
||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
||||
dialog.dismiss()
|
||||
callback(password)
|
||||
@@ -371,7 +372,7 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
toast(getString(R.string.password_cannot_be_empty))
|
||||
}
|
||||
}
|
||||
box?.setOnEditorActionListener { _, actionId, _ ->
|
||||
box.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
handleOkAction()
|
||||
true
|
||||
@@ -379,9 +380,8 @@ class SettingsCommonActivity : AppCompatActivity() {
|
||||
false
|
||||
}
|
||||
}
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
if (!isExporting) subtitleTextView?.text =
|
||||
dialogView.subtitle.visibility = View.VISIBLE
|
||||
if (!isExporting) dialogView.subtitle.text =
|
||||
getString(R.string.enter_password_to_decrypt_file)
|
||||
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.anime_add_repository),
|
||||
desc = getString(R.string.anime_add_repository),
|
||||
desc = getString(R.string.anime_add_repository_desc),
|
||||
icon = R.drawable.ic_github,
|
||||
onClick = {
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
@@ -198,7 +198,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.manga_add_repository),
|
||||
desc = getString(R.string.manga_add_repository),
|
||||
desc = getString(R.string.manga_add_repository_desc),
|
||||
icon = R.drawable.ic_github,
|
||||
onClick = {
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
@@ -234,7 +234,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.user_agent),
|
||||
desc = getString(R.string.NSFWExtention),
|
||||
desc = getString(R.string.user_agent_desc),
|
||||
icon = R.drawable.ic_round_video_settings_24,
|
||||
onClick = {
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
@@ -263,7 +263,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.force_legacy_installer),
|
||||
desc = getString(R.string.force_legacy_installer),
|
||||
desc = getString(R.string.force_legacy_installer_desc),
|
||||
icon = R.drawable.ic_round_new_releases_24,
|
||||
isChecked = extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY,
|
||||
switch = { isChecked, _ ->
|
||||
@@ -278,7 +278,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.skip_loading_extension_icons),
|
||||
desc = getString(R.string.skip_loading_extension_icons),
|
||||
desc = getString(R.string.skip_loading_extension_icons_desc),
|
||||
icon = R.drawable.ic_round_no_icon_24,
|
||||
isChecked = PrefManager.getVal(PrefName.SkipExtensionIcons),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -288,7 +288,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.NSFWExtention),
|
||||
desc = getString(R.string.NSFWExtention),
|
||||
desc = getString(R.string.NSFWExtention_desc),
|
||||
icon = R.drawable.ic_round_nsfw_24,
|
||||
isChecked = PrefManager.getVal(PrefName.NSFWExtension),
|
||||
switch = { isChecked, _ ->
|
||||
|
||||
@@ -67,7 +67,7 @@ class SettingsMangaActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.reader_settings),
|
||||
desc = getString(R.string.reader_settings),
|
||||
desc = getString(R.string.reader_settings_desc),
|
||||
icon = R.drawable.ic_round_reader_settings,
|
||||
onClick = {
|
||||
startActivity(Intent(context, ReaderSettingsActivity::class.java))
|
||||
@@ -77,7 +77,7 @@ class SettingsMangaActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.purge_manga_downloads),
|
||||
desc = getString(R.string.purge_manga_downloads),
|
||||
desc = getString(R.string.purge_manga_downloads_desc),
|
||||
icon = R.drawable.ic_round_delete_24,
|
||||
onClick = {
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
@@ -103,7 +103,7 @@ class SettingsMangaActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.purge_novel_downloads),
|
||||
desc = getString(R.string.purge_novel_downloads),
|
||||
desc = getString(R.string.purge_novel_downloads_desc),
|
||||
icon = R.drawable.ic_round_delete_24,
|
||||
onClick = {
|
||||
val dialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
@@ -128,7 +128,7 @@ class SettingsMangaActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.include_list),
|
||||
desc = getString(R.string.include_list),
|
||||
desc = getString(R.string.include_list_desc),
|
||||
icon = R.drawable.view_list_24,
|
||||
isChecked = PrefManager.getVal(PrefName.IncludeMangaList),
|
||||
switch = { isChecked, _ ->
|
||||
|
||||
@@ -105,7 +105,7 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.anilist_notification_filters),
|
||||
desc = getString(R.string.anilist_notification_filters),
|
||||
desc = getString(R.string.anilist_notification_filters_desc),
|
||||
icon = R.drawable.ic_anilist,
|
||||
onClick = {
|
||||
val types = NotificationType.entries.map { it.name }
|
||||
@@ -138,10 +138,7 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
R.string.anilist_notifications_checking_time,
|
||||
aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
|
||||
),
|
||||
desc = getString(
|
||||
R.string.anilist_notifications_checking_time,
|
||||
aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
|
||||
),
|
||||
desc = getString(R.string.anilist_notifications_checking_time_desc),
|
||||
icon = R.drawable.ic_round_notifications_none_24,
|
||||
onClick = {
|
||||
val selected =
|
||||
@@ -173,10 +170,7 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
R.string.comment_notification_checking_time,
|
||||
cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)]
|
||||
),
|
||||
desc = getString(
|
||||
R.string.comment_notification_checking_time,
|
||||
cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)]
|
||||
),
|
||||
desc = getString(R.string.comment_notification_checking_time_desc),
|
||||
icon = R.drawable.ic_round_notifications_none_24,
|
||||
onClick = {
|
||||
val selected =
|
||||
@@ -205,7 +199,7 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.notification_for_checking_subscriptions),
|
||||
desc = getString(R.string.notification_for_checking_subscriptions),
|
||||
desc = getString(R.string.notification_for_checking_subscriptions_desc),
|
||||
icon = R.drawable.ic_round_smart_button_24,
|
||||
isChecked = PrefManager.getVal(PrefName.SubscriptionCheckingNotifications),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -221,7 +215,7 @@ class SettingsNotificationActivity : AppCompatActivity() {
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.use_alarm_manager_reliable),
|
||||
desc = getString(R.string.use_alarm_manager_reliable),
|
||||
desc = getString(R.string.use_alarm_manager_reliable_desc),
|
||||
icon = R.drawable.ic_anilist,
|
||||
isChecked = PrefManager.getVal(PrefName.UseAlarmManager),
|
||||
switch = { isChecked, view ->
|
||||
|
||||
@@ -112,7 +112,7 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.oled_theme_variant),
|
||||
desc = getString(R.string.oled_theme_variant),
|
||||
desc = getString(R.string.oled_theme_variant_desc),
|
||||
icon = R.drawable.ic_round_brightness_4_24,
|
||||
isChecked = PrefManager.getVal(PrefName.UseOLED),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -123,7 +123,7 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.use_material_you),
|
||||
desc = getString(R.string.use_material_you),
|
||||
desc = getString(R.string.use_material_you_desc),
|
||||
icon = R.drawable.ic_round_new_releases_24,
|
||||
isChecked = PrefManager.getVal(PrefName.UseMaterialYou),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -136,7 +136,7 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.use_unique_theme_for_each_item),
|
||||
desc = getString(R.string.use_unique_theme_for_each_item),
|
||||
desc = getString(R.string.use_unique_theme_for_each_item_desc),
|
||||
icon = R.drawable.ic_palette,
|
||||
isChecked = PrefManager.getVal(PrefName.UseSourceTheme),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -147,7 +147,7 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.use_custom_theme),
|
||||
desc = getString(R.string.use_custom_theme),
|
||||
desc = getString(R.string.use_custom_theme_desc),
|
||||
icon = R.drawable.ic_palette,
|
||||
isChecked = PrefManager.getVal(PrefName.UseCustomTheme),
|
||||
switch = { isChecked, _ ->
|
||||
@@ -160,7 +160,7 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.color_picker),
|
||||
desc = getString(R.string.color_picker),
|
||||
desc = getString(R.string.color_picker_desc),
|
||||
icon = R.drawable.ic_palette,
|
||||
onClick = {
|
||||
val originalColor: Int = PrefManager.getVal(PrefName.CustomThemeInt)
|
||||
|
||||
@@ -10,7 +10,9 @@ import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.IntentSanitizer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.util.lang.use
|
||||
@@ -55,7 +57,16 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
}
|
||||
|
||||
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
|
||||
else -> continueQueue(InstallStep.Error)
|
||||
PackageInstaller.STATUS_FAILURE_CONFLICT -> {
|
||||
Logger.log("Failed to install extension due to conflict")
|
||||
toast(context.getString(R.string.failed_ext_install_conflict))
|
||||
continueQueue(InstallStep.Error)
|
||||
}
|
||||
else -> {
|
||||
Logger.log("Fatal error for $intent")
|
||||
Logger.log("Status: ${intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)}")
|
||||
continueQueue(InstallStep.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.novel.NovelExtension
|
||||
import ani.dantotsu.parsers.novel.NovelLoadResult
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
@@ -28,6 +30,7 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||
|
||||
private var animeListener: AnimeListener? = null
|
||||
private var mangaListener: MangaListener? = null
|
||||
private var novelListener: NovelListener? = null
|
||||
private var type: MediaType? = null
|
||||
|
||||
/**
|
||||
@@ -50,6 +53,12 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||
return this
|
||||
}
|
||||
|
||||
fun setNovelListener(listener: NovelListener): ExtensionInstallReceiver {
|
||||
this.type = MediaType.NOVEL
|
||||
novelListener = listener
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the events of the [filter] is received. When the package is an extension,
|
||||
* it's loaded in background and it notifies the [listener] when finished.
|
||||
@@ -92,6 +101,16 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||
}
|
||||
}
|
||||
|
||||
MediaType.NOVEL -> {
|
||||
when (val result = getNovelExtensionFromIntent(context, intent)) {
|
||||
is NovelLoadResult.Success -> novelListener?.onExtensionInstalled(
|
||||
result.extension
|
||||
)
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +139,16 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||
}
|
||||
}
|
||||
|
||||
MediaType.NOVEL -> {
|
||||
when (val result = getNovelExtensionFromIntent(context, intent)) {
|
||||
is NovelLoadResult.Success -> novelListener?.onExtensionUpdated(
|
||||
result.extension
|
||||
)
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +168,10 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||
mangaListener?.onPackageUninstalled(pkgName)
|
||||
}
|
||||
|
||||
MediaType.NOVEL -> {
|
||||
novelListener?.onPackageUninstalled(pkgName)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -188,6 +221,23 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||
}.await()
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private suspend fun getNovelExtensionFromIntent(
|
||||
context: Context,
|
||||
intent: Intent?
|
||||
): NovelLoadResult {
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName == null) {
|
||||
Logger.log("Package name not found")
|
||||
return NovelLoadResult.Error(Exception("Package name not found"))
|
||||
}
|
||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
||||
ExtensionLoader.loadNovelExtensionFromPkgName(
|
||||
context,
|
||||
pkgName,
|
||||
)
|
||||
}.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that receives extension installation events.
|
||||
@@ -206,6 +256,12 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
|
||||
interface NovelListener {
|
||||
fun onExtensionInstalled(extension: NovelExtension.Installed)
|
||||
fun onExtensionUpdated(extension: NovelExtension.Installed)
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,6 +87,8 @@ class ExtensionInstaller(private val context: Context) {
|
||||
downloadUri.lastPathSegment
|
||||
)
|
||||
.setDescription(type.asText())
|
||||
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
|
||||
.setAllowedOverRoaming(true)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
|
||||
val id = downloadManager.enqueue(request)
|
||||
|
||||
@@ -6,6 +6,9 @@ import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.NovelInterface
|
||||
import ani.dantotsu.parsers.novel.NovelExtension
|
||||
import ani.dantotsu.parsers.novel.NovelLoadResult
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
@@ -24,6 +27,7 @@ import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions. Supports two kinds of extensions:
|
||||
@@ -77,6 +81,10 @@ internal object ExtensionLoader {
|
||||
private const val officialSignatureManga =
|
||||
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||
|
||||
//dan's key
|
||||
private const val officialSignature =
|
||||
"a3061edb369278749b8e8de810d440d38e96417bbd67bbdfc5d9d9ed475ce4a5"
|
||||
|
||||
/**
|
||||
* List of the trusted signatures.
|
||||
*/
|
||||
@@ -133,6 +141,28 @@ internal object ExtensionLoader {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNovelExtensions(context: Context): List<NovelLoadResult> {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
|
||||
} else {
|
||||
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
||||
}
|
||||
|
||||
val extPkgs = installedPkgs.filter { isPackageAnExtension(MediaType.NOVEL, it) }
|
||||
|
||||
if (extPkgs.isEmpty()) return emptyList()
|
||||
|
||||
// Load each extension concurrently and wait for completion
|
||||
return runBlocking {
|
||||
val deferred = extPkgs.map {
|
||||
async { loadNovelExtension(context, it.packageName, it) }
|
||||
}
|
||||
deferred.map { it.await() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load an extension from the given package name. It checks if the extension
|
||||
* contains the required feature flag before trying to load it.
|
||||
@@ -167,6 +197,21 @@ internal object ExtensionLoader {
|
||||
return loadMangaExtension(context, pkgName, pkgInfo)
|
||||
}
|
||||
|
||||
fun loadNovelExtensionFromPkgName(context: Context, pkgName: String): NovelLoadResult {
|
||||
val pkgInfo = try {
|
||||
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||
} catch (error: PackageManager.NameNotFoundException) {
|
||||
// Unlikely, but the package may have been uninstalled at this point
|
||||
Logger.log(error)
|
||||
return NovelLoadResult.Error(error)
|
||||
}
|
||||
if (!isPackageAnExtension(MediaType.NOVEL, pkgInfo)) {
|
||||
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
||||
return NovelLoadResult.Error(Exception("Tried to load a package that wasn't a extension ($pkgName)"))
|
||||
}
|
||||
return loadNovelExtension(context, pkgName, pkgInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an extension given its package name.
|
||||
*
|
||||
@@ -400,17 +445,75 @@ internal object ExtensionLoader {
|
||||
return MangaLoadResult.Success(extension)
|
||||
}
|
||||
|
||||
private fun loadNovelExtension(
|
||||
context: Context,
|
||||
pkgName: String,
|
||||
pkgInfo: PackageInfo
|
||||
): NovelLoadResult {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
val appInfo = try {
|
||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||
} catch (error: PackageManager.NameNotFoundException) {
|
||||
// Unlikely, but the package may have been uninstalled at this point
|
||||
Logger.log(error)
|
||||
return NovelLoadResult.Error(error)
|
||||
}
|
||||
|
||||
val extName =
|
||||
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
Logger.log("Missing versionName for extension $extName")
|
||||
return NovelLoadResult.Error(Exception("Missing versionName for extension $extName"))
|
||||
}
|
||||
|
||||
val signatureHash = getSignatureHash(pkgInfo)
|
||||
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
val novelInterfaceInstance = try {
|
||||
val className = appInfo.loadLabel(context.packageManager).toString()
|
||||
val extensionClassName =
|
||||
"some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className"
|
||||
val loadedClass = classLoader.loadClass(extensionClassName)
|
||||
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||
instance as? NovelInterface
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Extension load error: $extName")
|
||||
return NovelLoadResult.Error(e as Exception)
|
||||
}
|
||||
|
||||
val extension = NovelExtension.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
sources = listOfNotNull(novelInterfaceInstance),
|
||||
isUnofficial = signatureHash != officialSignatureManga,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
return NovelLoadResult.Success(extension)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the given package is an extension.
|
||||
*
|
||||
* @param pkgInfo The package info of the application.
|
||||
*/
|
||||
private fun isPackageAnExtension(type: MediaType, pkgInfo: PackageInfo): Boolean {
|
||||
return pkgInfo.reqFeatures.orEmpty().any {
|
||||
it.name == when (type) {
|
||||
MediaType.ANIME -> ANIME_PACKAGE
|
||||
MediaType.MANGA -> MANGA_PACKAGE
|
||||
else -> ""
|
||||
|
||||
return if (type == MediaType.NOVEL) {
|
||||
pkgInfo.packageName.startsWith("some.random")
|
||||
} else {
|
||||
pkgInfo.reqFeatures.orEmpty().any {
|
||||
it.name == when (type) {
|
||||
MediaType.ANIME -> ANIME_PACKAGE
|
||||
MediaType.MANGA -> MANGA_PACKAGE
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
app/src/main/res/drawable/ic_round_magnet_24.xml
Normal file
12
app/src/main/res/drawable/ic_round_magnet_24.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.7,12.818a1.022,1.022 0,0 1,0 1.445L20.154,15.81l-3.589,-3.589 1.547,-1.548a1.022,1.022 0,0 1,1.444 0ZM9.737,2.3 L8.19,3.846l3.59,3.589 1.546,-1.547a1.021,1.021 0,0 0,0 -1.444L11.181,2.3A1.021,1.021 0,0 0,9.737 2.3ZM4.478,19.522a8.458,8.458 0,0 0,11.963 0l2.269,-2.268 -3.589,-3.589 -2.269,2.268a3.384,3.384 0,0 1,-4.785 -4.785l2.269,-2.269L6.747,5.29 4.478,7.559A8.458,8.458 0,0 0,4.478 19.522Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -66,7 +66,7 @@
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="1"
|
||||
tools:itemCount="5"
|
||||
tools:listitem="@layout/item_settings" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
android:layout_gravity="center"
|
||||
android:drawablePadding="4dp"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text="Image"
|
||||
android:text="@string/image"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
app:drawableStartCompat="@drawable/ic_round_search_24"
|
||||
app:drawableTint="?attr/colorPrimary" />
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
<string name="noSdFound">No SD card was Found.</string>
|
||||
<string name="reader_settings">Reader Settings</string>
|
||||
<string name="default_source">Default Source</string>
|
||||
<string name="show_yt">Show Youtube Link</string>
|
||||
<string name="show_yt">Youtube Link</string>
|
||||
<string name="default_ep_view">Default Episode Layout</string>
|
||||
<string name="default_chp_view">Default Chapter Layout</string>
|
||||
<string name="ui">User Interface</string>
|
||||
@@ -404,7 +404,7 @@
|
||||
|
||||
\n\n_It is not required to sync both MAL and Anilist accounts._
|
||||
</string>
|
||||
<string name="notification_for_checking_subscriptions">Show notification for Checking Subscriptions</string>
|
||||
<string name="notification_for_checking_subscriptions">Checking Subscriptions notification</string>
|
||||
<string name="use_alarm_manager">Use Alarm Manager</string>
|
||||
<string name="use_alarm_manager_reliable">Use Alarm Manager for reliable Notifications</string>
|
||||
<string name="use_alarm_manager_confirm">Using Alarm Manger can help fight against battery optimization, but may consume more battery. It also requires the Alarm Manager permission.</string>
|
||||
@@ -793,7 +793,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
|
||||
<string name="blur">Blur</string>
|
||||
<string name="hide_scroll_bar">Hide Scroll Bar</string>
|
||||
<string name="view_on_anilist">View on AniList</string>
|
||||
<string name="anilist_notification_filters">Choose category to ignore</string>
|
||||
<string name="anilist_notification_filters">Filter Notifications</string>
|
||||
<string name="anilist_notifications_checking_time">Anilist notifications update frequency : %1$s</string>
|
||||
<string name="comment_notification_checking_time">Comment notifications update frequency : %1$s</string>
|
||||
<string name="activities">Activities</string>
|
||||
@@ -912,13 +912,50 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
|
||||
<string name="devs_desc">Dantotsu\'s very own unpaid labours </string>
|
||||
<string name="forks_desc">More like Dantotsu</string>
|
||||
<string name="disclaimer_desc">Something to keep in mind</string>
|
||||
<string name="enable_torrent_desc">This will run a torrent server in the background</string>
|
||||
<string name="player_settings_desc">All the player related settings</string>
|
||||
<string name="purge_anime_downloads_desc">Delete all downloaded Anime</string>
|
||||
<string name="prefer_dub_desc">Select Dub media by default</string>
|
||||
<string name="show_yt_desc">Show YouTube links for supported media</string>
|
||||
<string name="include_list_anime_desc">Include list items in Anime explore page</string>
|
||||
<string name="ui_settings_desc">All the UI related settings</string>
|
||||
<string name="download_manager_select_desc">Default manager to download Anime</string>
|
||||
<string name="backup_restore_desc">Your preferred settings everywhere</string>
|
||||
<string name="change_download_location_desc">Change the location where all the downloaded stuff goes</string>
|
||||
<string name="always_continue_content_desc">Continue where you left off</string>
|
||||
<string name="search_source_list_desc">Auto-select the next server if the previous server doesn\'t have the media</string>
|
||||
<string name="recentlyListOnly_desc">Show only your list items in the recent tab</string>
|
||||
<string name="adult_only_content_desc">Show only adult content in the explore page</string>
|
||||
<string name="anime_add_repository_desc">Add Anime Extensions from various sources</string>
|
||||
<string name="manga_add_repository_desc">Add Manga Extensions from various sources</string>
|
||||
<string name="user_agent_desc">Change your default user agent</string>
|
||||
<string name="force_legacy_installer_desc">Use the legacy installer to install extensions (For older android phones)</string>
|
||||
<string name="skip_loading_extension_icons_desc">Don\'t load icons of extensions on the extension page</string>
|
||||
<string name="NSFWExtention_desc">Remove 18+ extensions from search (May remove some useful extensions)</string>
|
||||
<string name="reader_settings_desc">All the Reader related settings</string>
|
||||
<string name="purge_manga_downloads_desc">Delete all downloaded Manga</string>
|
||||
<string name="purge_novel_downloads_desc">Delete all downloaded Novels</string>
|
||||
<string name="include_list_desc">Include list items in Manga explore page</string>
|
||||
<string name="anilist_notification_filters_desc">Choose category to Ignore</string>
|
||||
<string name="anilist_notifications_checking_time_desc">Set the frequency you want the app to check for Anilist notifications</string>
|
||||
<string name="comment_notification_checking_time_desc">Set the frequency you want the app to check for Comment notifications</string>
|
||||
<string name="notification_for_checking_subscriptions_desc">Show notification for checking Subscriptions</string>
|
||||
<string name="use_alarm_manager_reliable_desc">Use to bypass the phone\'s battery optimizer</string>
|
||||
<string name="oled_theme_variant_desc">As dark as it gets</string>
|
||||
<string name="use_material_you_desc">Use the same color as your wallpaper</string>
|
||||
<string name="use_unique_theme_for_each_item_desc">Use color from media\'s banner</string>
|
||||
<string name="use_custom_theme_desc">Use your own color for the theme</string>
|
||||
<string name="color_picker_desc">Choose a color</string>
|
||||
<string name="torrent_addon">Torrent Addon</string>
|
||||
<string name="enable_torrent">Enable torrent</string>
|
||||
<string name="anime_downloader_addon">Anime Downloader Addon</string>
|
||||
<string name="install_torrent_addon">Install Torrent Addon</string>
|
||||
<string name="loaded_successfully">Loaded Successfully</string>
|
||||
<string name="not_installed">Not Installed</string>
|
||||
<string name="torrent_extension_not_supported">Torrent extension not supported on this device</string>
|
||||
<string name="update_addon">Update Addon</string>
|
||||
<string name="install_addon">Install Addon</string>
|
||||
<string name="download_addon_not_found">Download addon not found</string>
|
||||
<string name="image">Image</string>
|
||||
<string name="failed_ext_install_conflict">Failed to install extension due to conflict</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user