Compare commits

...

10 Commits

Author SHA1 Message Date
rebelonion
870cb751a4 fix: duplicate download 2024-04-21 08:06:23 -05:00
rebelonion
4ffe9d7505 fix: novel loading 2024-04-21 07:36:23 -05:00
rebelonion
513b937e59 fix: some sorting problems 2024-04-21 06:41:51 -05:00
rebelonion
6113a10556 fix: update spinner 2024-04-21 06:21:10 -05:00
rebelonion
233f4bfb48 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-21 06:06:01 -05:00
aayush262
3fd01d582a fix: forgot to remove todo strings 2024-04-21 16:22:02 +05:30
aayush262
00758af458 feat: desc for every setting 2024-04-21 16:02:21 +05:30
rebelonion
4477e3a0e1 fix: view clickable after hidden
https://stackoverflow.com/questions/4728908/android-view-with-view-gone-still-receives-ontouch-and-onclick
2024-04-21 04:50:55 -05:00
rebelonion
e475cc5c01 fix: novel extension installing 2024-04-21 04:31:24 -05:00
rebelonion
3622d91886 fix: allow deprecated media to be played 2024-04-21 02:58:17 -05:00
45 changed files with 1008 additions and 816 deletions

View File

@@ -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 {

View File

@@ -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')

View File

@@ -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) {} },

View File

@@ -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 -> {

View File

@@ -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

View File

@@ -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

View 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"
}
}

View File

@@ -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()
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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
}

View File

@@ -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()
}
}
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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)
}
}
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -121,7 +121,6 @@ class CommentsFragment : Fragment() {
}
}
} else {
toast("Not logged in")
activity.binding.commentMessageContainer.visibility = View.GONE
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 }

View File

@@ -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://"
}
}

View 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()
}

View File

@@ -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())
}
}
}
}
/**

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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, _ ->

View File

@@ -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)

View 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, _ ->

View File

@@ -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, _ ->

View File

@@ -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 ->

View File

@@ -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)

View File

@@ -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)
}
}
}
}

View File

@@ -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 {
/**

View File

@@ -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)

View File

@@ -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 -> ""
}
}
}
}

View 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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>