diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index da9dac0d..d87bab8f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,8 +9,7 @@
+ android:maxSdkVersion="32" />
diff --git a/app/src/main/java/ani/dantotsu/App.kt b/app/src/main/java/ani/dantotsu/App.kt
index 9b57909c..b5c7ebad 100644
--- a/app/src/main/java/ani/dantotsu/App.kt
+++ b/app/src/main/java/ani/dantotsu/App.kt
@@ -8,8 +8,8 @@ import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication
import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
-import ani.dantotsu.aniyomi.data.Notifications
-import ani.dantotsu.aniyomi.util.logcat
+import eu.kanade.tachiyomi.data.notification.Notifications
+import tachiyomi.core.util.system.logcat
import ani.dantotsu.others.DisabledReports
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt
index edbf4dcc..e81b05d2 100644
--- a/app/src/main/java/ani/dantotsu/MainActivity.kt
+++ b/app/src/main/java/ani/dantotsu/MainActivity.kt
@@ -24,7 +24,7 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
-import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
+import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding
@@ -37,13 +37,16 @@ import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.parsers.AnimeSources
+import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
+import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
@@ -58,6 +61,7 @@ class MainActivity : AppCompatActivity() {
private var uiSettings = UserInterfaceSettings()
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
+ private val mangaExtensionManager: MangaExtensionManager by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -65,11 +69,17 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
- val myScope = CoroutineScope(Dispatchers.Default)
- myScope.launch {
+ val animeScope = CoroutineScope(Dispatchers.Default)
+ animeScope.launch {
animeExtensionManager.findAvailableExtensions()
+ logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
-
+ }
+ val mangaScope = CoroutineScope(Dispatchers.Default)
+ mangaScope.launch {
+ mangaExtensionManager.findAvailableExtensions()
+ logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
+ MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
}
var doubleBackToExitPressedOnce = false
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/LICENSE b/app/src/main/java/ani/dantotsu/aniyomi/LICENSE
deleted file mode 100644
index 2bb9ad24..00000000
--- a/app/src/main/java/ani/dantotsu/aniyomi/LICENSE
+++ /dev/null
@@ -1,176 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/NOTICE.md b/app/src/main/java/ani/dantotsu/aniyomi/NOTICE.md
deleted file mode 100644
index 5a81820b..00000000
--- a/app/src/main/java/ani/dantotsu/aniyomi/NOTICE.md
+++ /dev/null
@@ -1,3 +0,0 @@
-NOTICE
-
-This software includes code modified from Aniyomi, available at https://github.com/aniyomiorg/aniyomi/.
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/App.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/App.kt
deleted file mode 100644
index 876e20c7..00000000
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/App.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package ani.dantotsu.aniyomi.anime.custom
-/*
-import android.app.Application
-import ani.dantotsu.aniyomi.data.Notifications
-import ani.dantotsu.aniyomi.util.logcat
-import logcat.AndroidLogcatLogger
-import logcat.LogPriority
-import logcat.LogcatLogger
-import uy.kohesive.injekt.Injekt
-
-class App : Application() {
- override fun onCreate() {
- super.onCreate()
- Injekt.importModule(AppModule(this))
- Injekt.importModule(PreferenceModule(this))
-
- setupNotificationChannels()
- if (!LogcatLogger.isInstalled) {
- LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
- }
- }
-
- private fun setupNotificationChannels() {
- try {
- Notifications.createChannels(this)
- } catch (e: Exception) {
- logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
- }
- }
-}*/
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt
index e4b3e8eb..bff74eeb 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt
+++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt
@@ -1,11 +1,12 @@
package ani.dantotsu.aniyomi.anime.custom
import android.app.Application
-import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
-import ani.dantotsu.aniyomi.core.preference.PreferenceStore
-import ani.dantotsu.aniyomi.domain.base.BasePreferences
-import ani.dantotsu.aniyomi.domain.source.service.SourcePreferences
-import ani.dantotsu.aniyomi.core.preference.AndroidPreferenceStore
+import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
+import tachiyomi.core.preference.PreferenceStore
+import eu.kanade.domain.base.BasePreferences
+import eu.kanade.domain.source.service.SourcePreferences
+import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
+import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.api.InjektModule
@@ -22,6 +23,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { AnimeExtensionManager(app) }
+ addSingletonFactory { MangaExtensionManager(app) }
+
addSingletonFactory {
Json {
ignoreUnknownKeys = true
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/srcapi/RxExtension.kt b/app/src/main/java/ani/dantotsu/aniyomi/util/srcapi/RxExtension.kt
deleted file mode 100644
index 220e6185..00000000
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/srcapi/RxExtension.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package ani.dantotsu.aniyomi.util.srcapi
-
-//actual suspend fun Observable.awaitSingle(): T = awaitSingle()
diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
index 6d71b69c..ea00c624 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
@@ -238,7 +238,7 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
return tryWithSuspend(true) {
chapter.addImages(
- mangaReadSources?.get(selected.source)?.loadImages(chapter.link) ?: return@tryWithSuspend false
+ mangaReadSources?.get(selected.source)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
)
if (post) mangaChapter.postValue(chapter)
true
diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt
index d16af066..16e51797 100644
--- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt
@@ -2,6 +2,7 @@ package ani.dantotsu.media.manga
import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaImage
+import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable
import kotlin.math.floor
@@ -10,8 +11,9 @@ data class MangaChapter(
var link: String,
var title: String? = null,
var description: String? = null,
+ var sChapter: SChapter
) : Serializable {
- constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description)
+ constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)
private val images = mutableListOf()
fun images(): List = images
diff --git a/app/src/main/java/ani/dantotsu/others/Jikan.kt b/app/src/main/java/ani/dantotsu/others/Jikan.kt
index eb56e8c0..e08231a0 100644
--- a/app/src/main/java/ani/dantotsu/others/Jikan.kt
+++ b/app/src/main/java/ani/dantotsu/others/Jikan.kt
@@ -25,7 +25,7 @@ object Jikan {
val ep = it.malID.toString()
eps[ep] = Episode(ep, title = it.title,
//Personal revenge with 34566 :prayge:
- filler = if(malId!=34566) it.filler else true
+ filler = if(malId!=34566) it.filler else true,
)
}
hasNextPage = res?.pagination?.hasNextPage == true
diff --git a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
index 45431e79..1b66928c 100644
--- a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
@@ -1,34 +1,11 @@
package ani.dantotsu.parsers
import ani.dantotsu.Lazier
-import ani.dantotsu.aniyomi.anime.model.AnimeExtension
+import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.lazyList
-//import ani.dantotsu.parsers.anime.AllAnime
-//import ani.dantotsu.parsers.anime.AnimeDao
-//import ani.dantotsu.parsers.anime.AnimePahe
-//import ani.dantotsu.parsers.anime.Gogo
-//import ani.dantotsu.parsers.anime.Haho
-//import ani.dantotsu.parsers.anime.HentaiFF
-//import ani.dantotsu.parsers.anime.HentaiMama
-//import ani.dantotsu.parsers.anime.HentaiStream
-//import ani.dantotsu.parsers.anime.Marin
-//import ani.dantotsu.parsers.anime.AniWave
-//import ani.dantotsu.parsers.anime.Kaido
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
-/*
-object AnimeSources_old : WatchSources() {
- override val list: List> = lazyList(
- "AllAnime" to ::AllAnime,
- "Gogo" to ::Gogo,
- "Kaido" to ::Kaido,
- "Marin" to ::Marin,
- "AnimePahe" to ::AnimePahe,
- "AniWave" to ::AniWave,
- "AnimeDao" to ::AnimeDao,
- )
-}
-*/
+
object AnimeSources : WatchSources() {
override var list: List> = emptyList()
@@ -52,13 +29,8 @@ object AnimeSources : WatchSources() {
}
-
object HAnimeSources : WatchSources() {
private val aList: List> = lazyList(
- //"HentaiMama" to ::HentaiMama,
- //"Haho" to ::Haho,
- //"HentaiStream" to ::HentaiStream,
- //"HentaiFF" to ::HentaiFF,
)
override val list = listOf(aList,AnimeSources.list).flatten()
diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
index 4fab3116..a93607c6 100644
--- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
@@ -1,10 +1,18 @@
package ani.dantotsu.parsers
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
import android.widget.Toast
import ani.dantotsu.FileUrl
-import ani.dantotsu.aniyomi.anime.model.AnimeExtension
-import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource
-import ani.dantotsu.aniyomi.util.network.interceptor.CloudflareBypassException
+import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
+import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
+import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import ani.dantotsu.currContext
import ani.dantotsu.logger
import eu.kanade.tachiyomi.animesource.model.SEpisode
@@ -13,6 +21,22 @@ import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
+import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.util.lang.awaitSingle
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.FileOutputStream
+import java.io.OutputStream
import java.net.URL
import java.net.URLDecoder
@@ -91,7 +115,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
}
- fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List {
+ private fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List {
return animesPage.animes.map { sAnime ->
// Extract required fields from sAnime
val name = sAnime.title
@@ -106,34 +130,160 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
}
}
- fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
- val episode = Episode(
- sEpisode.episode_number.toString(),
+ private fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
+ //if the float episode number is a whole number, convert it to an int
+ val episodeNumberInt =
+ if (sEpisode.episode_number % 1 == 0f) {
+ sEpisode.episode_number.toInt()
+ } else {
+ sEpisode.episode_number
+ }
+ return Episode(
+ episodeNumberInt.toString(),
sEpisode.url,
sEpisode.name,
null,
null,
false,
null,
- sEpisode)
- return episode
+ sEpisode
+ )
}
- fun VideoToVideoServer(video: Video): VideoServer {
- val videoServer = VideoServer(
+ private fun VideoToVideoServer(video: Video): VideoServer {
+ return VideoServer(
video.quality,
video.url,
null,
- video)
- return videoServer
+ video
+ )
}
}
-class VideoServerPassthrough : VideoExtractor{
- val videoServer: VideoServer
- constructor(videoServer: VideoServer) {
- this.videoServer = videoServer
+class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
+ val extension: MangaExtension.Installed
+ init {
+ this.extension = extension
}
+ override val name = extension.name
+ override val saveName = extension.name
+ override val hostUrl = extension.sources.first().name
+
+ override suspend fun loadChapters(mangaLink: String, extra: Map?, sManga: SManga): List {
+ val source = extension.sources.first()
+ if (source is CatalogueSource) {
+ try {
+ val res = source.getChapterList(sManga)
+ var chapterList: List = emptyList()
+ for (chapter in res) {
+ chapterList += SChapterToMangaChapter(chapter)
+ }
+ logger("chapterList size: ${chapterList.size}")
+ return chapterList
+ }
+ catch (e: Exception) {
+ logger("loadChapters Exception: $e")
+ }
+ return emptyList()
+ }
+ return emptyList() // Return an empty list if source is not a catalogueSource
+ }
+
+ override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List {
+ val source = extension.sources.first()
+ if (source is HttpSource) {
+ //try {
+ val res = source.getPageList(sChapter)
+ var chapterList: List = emptyList()
+ for (page in res) {
+ println("page: $page")
+ currContext()?.let { fetchAndProcessImage(page, source, it.contentResolver) }
+ logger("new image url: ${page.imageUrl}")
+ chapterList += PageToMangaImage(page)
+ }
+ logger("image url: chapterList size: ${chapterList.size}")
+ return chapterList
+ //}
+ //catch (e: Exception) {
+ // logger("loadImages Exception: $e")
+ //}
+ return emptyList()
+ }
+ return emptyList() // Return an empty list if source is not a CatalogueSource
+ }
+
+
+ override suspend fun search(query: String): List {
+ val source = extension.sources.first()
+ if (source is HttpSource) {
+ var res: MangasPage? = null
+ try {
+ res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
+ logger("res observable: $res")
+ }
+ catch (e: CloudflareBypassException) {
+ logger("Exception in search: $e")
+ Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
+ }
+
+ val conv = convertMangasPageToShowResponse(res!!)
+ return conv
+ }
+ return emptyList() // Return an empty list if source is not a CatalogueSource
+ }
+
+ private fun convertMangasPageToShowResponse(mangasPage: MangasPage): List {
+ return mangasPage.mangas.map { sManga ->
+ // Extract required fields from sManga
+ val name = sManga.title
+ val link = sManga.url
+ val coverUrl = sManga.thumbnail_url ?: ""
+ val otherNames = emptyList() // Populate as needed
+ val total = 20
+ val extra: Map? = null // Populate as needed
+
+ // Create a new ShowResponse
+ ShowResponse(name, link, coverUrl, sManga)
+ }
+ }
+
+ private fun PageToMangaImage(page: Page): MangaImage {
+ //find and move any headers from page.imageUrl to headersMap
+ val headersMap: Map = page.imageUrl?.split("&")?.mapNotNull {
+ val idx = it.indexOf("=")
+ if (idx != -1) {
+ val key = URLDecoder.decode(it.substring(0, idx), "UTF-8")
+ val value = URLDecoder.decode(it.substring(idx + 1), "UTF-8")
+ Pair(key, value)
+ } else {
+ null // Or some other default value
+ }
+ }?.toMap() ?: mapOf()
+ val urlWithoutHeaders = page.imageUrl?.split("&")?.get(0) ?: ""
+ val url = page.imageUrl ?: ""
+ logger("Pageurl: $url")
+ logger("regularurl: ${page.url}")
+ logger("regularurl: ${page.status}")
+ return MangaImage(
+ FileUrl(url, headersMap),
+ false,
+ page
+ )
+ }
+
+ private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
+ return MangaChapter(
+ sChapter.name,
+ sChapter.url,
+ sChapter.name,
+ null,
+ sChapter
+ )
+ }
+
+}
+
+class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
override val server: VideoServer
get() {
return videoServer
diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
index b6bb5063..5aa826b1 100644
--- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
@@ -3,6 +3,7 @@ package ani.dantotsu.parsers
import ani.dantotsu.*
import ani.dantotsu.media.Media
import eu.kanade.tachiyomi.animesource.model.SAnime
+import eu.kanade.tachiyomi.source.model.SManga
import java.io.Serializable
import java.net.URLDecoder
import java.net.URLEncoder
@@ -141,7 +142,10 @@ data class ShowResponse(
val extra : Map?=null,
//SAnime object from Aniyomi
- val sAnime: SAnime?=null
+ val sAnime: SAnime? = null,
+
+ //SManga object from Aniyomi
+ val sManga: SManga? = null
) : Serializable {
constructor(name: String, link: String, coverUrl: String, otherNames: List = listOf(), total: Int? = null, extra: Map?=null)
: this(name, link, FileUrl(coverUrl), otherNames, total, extra)
@@ -157,6 +161,9 @@ data class ShowResponse(
constructor(name: String, link: String, coverUrl: String, sAnime: SAnime)
: this(name, link, FileUrl(coverUrl), sAnime = sAnime)
+
+ constructor(name: String, link: String, coverUrl: String, sManga: SManga)
+ : this(name, link, FileUrl(coverUrl), sManga = sManga)
}
diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt
index 285630a5..fb612bb3 100644
--- a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt
@@ -1,6 +1,7 @@
package ani.dantotsu.parsers
import ani.dantotsu.Lazier
+import ani.dantotsu.logger
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.Media
@@ -52,11 +53,17 @@ abstract class MangaReadSources : BaseSources() {
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap {
val map = mutableMapOf()
val parser = get(i)
- tryWithSuspend(true) {
- parser.loadChapters(show.link, show.extra).forEach {
- map[it.number] = MangaChapter(it)
+ show.sManga?.let { sManga ->
+ tryWithSuspend(true) {
+ parser.loadChapters(show.link, show.extra, sManga).forEach {
+ map[it.number] = MangaChapter(it)
+ }
}
}
+ if(show.sManga == null) {
+ logger("sManga is null")
+ }
+ logger("map size ${map.size}")
return map
}
}
diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt
index 670fbda2..81de230b 100644
--- a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt
@@ -3,6 +3,9 @@ package ani.dantotsu.parsers
import ani.dantotsu.FileUrl
import ani.dantotsu.media.Media
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
import java.io.Serializable
abstract class MangaParser : BaseParser() {
@@ -10,7 +13,7 @@ abstract class MangaParser : BaseParser() {
/**
* Takes ShowResponse.link and ShowResponse.extra (if any) as arguments & gives a list of total chapters present on the site.
* **/
- abstract suspend fun loadChapters(mangaLink: String, extra: Map?): List
+ abstract suspend fun loadChapters(mangaLink: String, extra: Map?, sManga: SManga): List
/**
* Takes ShowResponse.link, ShowResponse.extra & the Last Largest Chapter Number known by app as arguments
@@ -18,8 +21,8 @@ abstract class MangaParser : BaseParser() {
* Returns the latest chapter (If overriding, Make sure the chapter is actually the latest chapter)
* Returns null, if no latest chapter is found.
* **/
- open suspend fun getLatestChapter(mangaLink: String, extra: Map?, latest: Float): MangaChapter? {
- return loadChapters(mangaLink, extra)
+ open suspend fun getLatestChapter(mangaLink: String, extra: Map?, sManga: SManga, latest: Float): MangaChapter? {
+ return loadChapters(mangaLink, extra, sManga)
.maxByOrNull { it.number.toFloatOrNull() ?: 0f }
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
}
@@ -27,7 +30,7 @@ abstract class MangaParser : BaseParser() {
/**
* Takes MangaChapter.link as an argument & returns a list of MangaImages with their Url (with headers & transformations, if needed)
* **/
- abstract suspend fun loadImages(chapterLink: String): List
+ abstract suspend fun loadImages(chapterLink: String, sChapter: SChapter): List
override suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response = loadSavedShowResponse(mediaObj.id)
@@ -65,6 +68,8 @@ data class MangaChapter(
//Self-Descriptive
val title: String? = null,
val description: String? = null,
+
+ val sChapter: SChapter,
)
data class MangaImage(
@@ -75,8 +80,10 @@ data class MangaImage(
* **/
val url: FileUrl,
- val useTransformation: Boolean = false
+ val useTransformation: Boolean = false,
+
+ val page: Page
) : Serializable{
- constructor(url: String,useTransformation: Boolean=false)
- : this(FileUrl(url),useTransformation)
+ constructor(url: String,useTransformation: Boolean=false, page: Page)
+ : this(FileUrl(url),useTransformation, page)
}
diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt
index cffca693..c25e0369 100644
--- a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt
+++ b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt
@@ -2,10 +2,30 @@ package ani.dantotsu.parsers
import ani.dantotsu.Lazier
import ani.dantotsu.lazyList
+import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() {
- override val list: List> = lazyList(
- )
+ override var list: List> = emptyList()
+
+ suspend fun init(fromExtensions: StateFlow>) {
+ // Initialize with the first value from StateFlow
+ val initialExtensions = fromExtensions.first()
+ list = createParsersFromExtensions(initialExtensions)
+
+ // Update as StateFlow emits new values
+ fromExtensions.collect { extensions ->
+ list = createParsersFromExtensions(extensions)
+ }
+ }
+
+ private fun createParsersFromExtensions(extensions: List): List> {
+ return extensions.map { extension ->
+ val name = extension.name
+ Lazier({ DynamicMangaParser(extension) }, name)
+ }
+ }
}
object HMangaSources : MangaReadSources() {
diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
index 76c6c47a..8f4a8184 100644
--- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt
@@ -3,13 +3,11 @@ package ani.dantotsu.settings
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context
-import android.content.Intent
+import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
-import android.net.Uri
import android.os.Build.*
import android.os.Build.VERSION.*
import android.os.Bundle
-import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -19,16 +17,19 @@ import android.widget.SearchView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
-import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
-import ani.dantotsu.aniyomi.anime.model.AnimeExtension
+import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
+import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.databinding.ActivityExtensionsBinding
import com.bumptech.glide.Glide
+import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
@@ -60,7 +61,7 @@ class ExtensionsActivity : AppCompatActivity() {
.subscribe(
{ installStep ->
val builder = NotificationCompat.Builder(this,
- ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_PROGRESS
+ Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(R.drawable.ic_round_sync_24)
.setContentTitle("Installing extension")
@@ -70,7 +71,7 @@ class ExtensionsActivity : AppCompatActivity() {
},
{ error ->
val builder = NotificationCompat.Builder(this,
- ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_ERROR
+ Notifications.CHANNEL_DOWNLOADER_ERROR
)
.setSmallIcon(R.drawable.ic_round_info_24)
.setContentTitle("Installation failed")
@@ -80,7 +81,7 @@ class ExtensionsActivity : AppCompatActivity() {
},
{
val builder = NotificationCompat.Builder(this,
- ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_PROGRESS)
+ Notifications.CHANNEL_DOWNLOADER_PROGRESS)
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
.setContentTitle("Installation complete")
.setContentText("The extension has been successfully installed.")
@@ -164,8 +165,11 @@ class ExtensionsActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed()
}
+
}
+
+
private class ExtensionsAdapter(private val onUninstallClicked: (String) -> Unit) : RecyclerView.Adapter() {
private var extensions: List = emptyList()
diff --git a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt
index b9d5b4fa..038f6598 100644
--- a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt
+++ b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt
@@ -66,7 +66,10 @@ class SubscriptionHelper {
val chp = withTimeoutOrNull(10 * 1000) {
tryWithSuspend {
val show = parser.loadSavedShowResponse(id) ?: throw Exception(currContext()?.getString(R.string.failed_to_load_data, id))
- parser.getLatestChapter(show.link, show.extra, selected.latest)
+ show.sManga?.let {
+ parser.getLatestChapter(show.link, show.extra,
+ it, selected.latest)
+ }
}
}
diff --git a/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt b/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt
new file mode 100644
index 00000000..2c641ccc
--- /dev/null
+++ b/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt
@@ -0,0 +1,38 @@
+package eu.kanade.core.preference
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import tachiyomi.core.preference.Preference
+
+class PreferenceMutableState(
+ private val preference: Preference,
+ scope: CoroutineScope,
+) : MutableState {
+
+ private val state = mutableStateOf(preference.get())
+
+ init {
+ preference.changes()
+ .onEach { state.value = it }
+ .launchIn(scope)
+ }
+
+ override var value: T
+ get() = state.value
+ set(value) {
+ preference.set(value)
+ }
+
+ override fun component1(): T {
+ return state.value
+ }
+
+ override fun component2(): (T) -> Unit {
+ return { preference.set(it) }
+ }
+}
+
+fun Preference.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/domain/base/BasePreferences.kt b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt
similarity index 90%
rename from app/src/main/java/ani/dantotsu/aniyomi/domain/base/BasePreferences.kt
rename to app/src/main/java/eu/kanade/domain/base/BasePreferences.kt
index c46c3c61..176322d4 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/domain/base/BasePreferences.kt
+++ b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt
@@ -1,9 +1,9 @@
-package ani.dantotsu.aniyomi.domain.base
+package eu.kanade.domain.base
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
-import ani.dantotsu.aniyomi.core.preference.PreferenceStore
+import tachiyomi.core.preference.PreferenceStore
class BasePreferences(
val context: Context,
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/domain/base/ExtensionInstallerPreference.kt b/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt
similarity index 81%
rename from app/src/main/java/ani/dantotsu/aniyomi/domain/base/ExtensionInstallerPreference.kt
rename to app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt
index e9c4592a..14dbe10e 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/domain/base/ExtensionInstallerPreference.kt
+++ b/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt
@@ -1,13 +1,13 @@
-package ani.dantotsu.aniyomi.domain.base
+package eu.kanade.domain.base
import android.content.Context
-import ani.dantotsu.aniyomi.util.system.hasMiuiPackageInstaller
-import ani.dantotsu.aniyomi.domain.base.BasePreferences.ExtensionInstaller
-import ani.dantotsu.aniyomi.util.system.isShizukuInstalled
+import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
+import eu.kanade.domain.base.BasePreferences.ExtensionInstaller
+import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import kotlinx.coroutines.CoroutineScope
-import ani.dantotsu.aniyomi.core.preference.Preference
-import ani.dantotsu.aniyomi.core.preference.PreferenceStore
-import ani.dantotsu.aniyomi.core.preference.getEnum
+import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.core.preference.getEnum
class ExtensionInstallerPreference(
private val context: Context,
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/domain/source/service/SetMigrateSorting.kt b/app/src/main/java/eu/kanade/domain/source/service/SetMigrateSorting.kt
similarity index 88%
rename from app/src/main/java/ani/dantotsu/aniyomi/domain/source/service/SetMigrateSorting.kt
rename to app/src/main/java/eu/kanade/domain/source/service/SetMigrateSorting.kt
index bafcb2f7..eb09eb42 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/domain/source/service/SetMigrateSorting.kt
+++ b/app/src/main/java/eu/kanade/domain/source/service/SetMigrateSorting.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.domain.source.service
+package eu.kanade.domain.source.service
class SetMigrateSorting(
private val preferences: SourcePreferences,
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt
similarity index 92%
rename from app/src/main/java/ani/dantotsu/aniyomi/domain/source/service/SourcePreferences.kt
rename to app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt
index 2835b77f..22a24479 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/domain/source/service/SourcePreferences.kt
+++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt
@@ -1,9 +1,9 @@
-package ani.dantotsu.aniyomi.domain.source.service
+package eu.kanade.domain.source.service
-import ani.dantotsu.aniyomi.core.preference.PreferenceStore
-import ani.dantotsu.aniyomi.util.system.LocaleHelper
-import ani.dantotsu.aniyomi.core.preference.getEnum
-import ani.dantotsu.aniyomi.domain.library.model.LibraryDisplayMode
+import eu.kanade.tachiyomi.util.system.LocaleHelper
+import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.core.preference.getEnum
+import tachiyomi.domain.library.model.LibraryDisplayMode
class SourcePreferences(
private val preferenceStore: PreferenceStore,
diff --git a/app/src/main/java/eu/kanade/domain/source/service/ToggleLanguage.kt b/app/src/main/java/eu/kanade/domain/source/service/ToggleLanguage.kt
new file mode 100644
index 00000000..3ebb3091
--- /dev/null
+++ b/app/src/main/java/eu/kanade/domain/source/service/ToggleLanguage.kt
@@ -0,0 +1,15 @@
+package eu.kanade.domain.source.service
+
+import tachiyomi.core.preference.getAndSet
+
+class ToggleLanguage(
+ val preferences: SourcePreferences,
+) {
+
+ fun await(language: String) {
+ val isEnabled = language in preferences.enabledLanguages().get()
+ preferences.enabledLanguages().getAndSet { enabled ->
+ if (isEnabled) enabled.minus(language) else enabled.plus(language)
+ }
+ }
+}
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/PreferenceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/PreferenceScreen.kt
similarity index 69%
rename from app/src/main/java/ani/dantotsu/aniyomi/PreferenceScreen.kt
rename to app/src/main/java/eu/kanade/tachiyomi/PreferenceScreen.kt
index 0eb65a07..8a867ba3 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/PreferenceScreen.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/PreferenceScreen.kt
@@ -1,3 +1,3 @@
-package ani.dantotsu.aniyomi
+package eu.kanade.tachiyomi
typealias PreferenceScreen = androidx.preference.PreferenceScreen
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeCatalogueSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeCatalogueSource.kt
similarity index 96%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeCatalogueSource.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeCatalogueSource.kt
index 412bce7b..67d99256 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeCatalogueSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeCatalogueSource.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.animesource
+package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt
similarity index 94%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeSource.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt
index c422c411..210e04df 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt
@@ -1,10 +1,9 @@
-package ani.dantotsu.aniyomi.animesource
+package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
-//import ani.dantotsu.aniyomi.util.awaitSingle
-import ani.dantotsu.aniyomi.util.lang.awaitSingle
+import eu.kanade.tachiyomi.util.lang.awaitSingle
import rx.Observable
/**
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeSourceFactory.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSourceFactory.kt
similarity index 84%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeSourceFactory.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSourceFactory.kt
index 14b550fe..ca5755b6 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/animesource/AnimeSourceFactory.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSourceFactory.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.animesource
+package eu.kanade.tachiyomi.animesource
/**
* A factory for creating sources at runtime.
diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/ConfigurableAnimeSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/ConfigurableAnimeSource.kt
index 11b88011..64f9e8a2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/animesource/ConfigurableAnimeSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/ConfigurableAnimeSource.kt
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.animesource
-import ani.dantotsu.aniyomi.animesource.AnimeSource
-import ani.dantotsu.aniyomi.PreferenceScreen
+import eu.kanade.tachiyomi.PreferenceScreen
interface ConfigurableAnimeSource : AnimeSource {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/AnimeFilter.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/AnimeFilter.kt
similarity index 100%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/model/AnimeFilter.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/model/AnimeFilter.kt
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/AnimeFilterList.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/AnimeFilterList.kt
similarity index 100%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/model/AnimeFilterList.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/model/AnimeFilterList.kt
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/AnimesPage.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/AnimesPage.kt
similarity index 100%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/model/AnimesPage.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/model/AnimesPage.kt
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/SAnime.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SAnime.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/model/SAnime.kt
index 607c00fc..80ee2c4d 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SAnime.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/SAnime.kt
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animesource.model
-import ani.dantotsu.aniyomi.source.model.UpdateStrategy
+import eu.kanade.tachiyomi.source.model.UpdateStrategy
import java.io.Serializable
interface SAnime : Serializable {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SAnimeImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/SAnimeImpl.kt
similarity index 90%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SAnimeImpl.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/model/SAnimeImpl.kt
index 05874e39..540522fc 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SAnimeImpl.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/SAnimeImpl.kt
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animesource.model
-import ani.dantotsu.aniyomi.source.model.UpdateStrategy
+import eu.kanade.tachiyomi.source.model.UpdateStrategy
class SAnimeImpl : SAnime {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SEpisode.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/SEpisode.kt
similarity index 100%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SEpisode.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/model/SEpisode.kt
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SEpisodeImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/SEpisodeImpl.kt
similarity index 100%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/model/SEpisodeImpl.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/model/SEpisodeImpl.kt
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/Video.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/Video.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/animesource/model/Video.kt
rename to app/src/main/java/eu/kanade/tachiyomi/animesource/model/Video.kt
index 1b077dbc..e1a8c60e 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/animesource/model/Video.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/model/Video.kt
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.animesource.model
import android.net.Uri
-import ani.dantotsu.aniyomi.util.network.ProgressListener
+import eu.kanade.tachiyomi.network.ProgressListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import okhttp3.Headers
diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt
index 31cfaf75..f8a38196 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animesource.online
-import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource
+import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/core/Constants.kt b/app/src/main/java/eu/kanade/tachiyomi/core/Constants.kt
similarity index 96%
rename from app/src/main/java/ani/dantotsu/aniyomi/core/Constants.kt
rename to app/src/main/java/eu/kanade/tachiyomi/core/Constants.kt
index 3c03bde5..f829cb7d 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/core/Constants.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/core/Constants.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.core
+package eu.kanade.tachiyomi.core
object Constants {
const val URL_HELP = "https://aniyomi.org/help/"
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/core/preference/AndroidPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt
similarity index 98%
rename from app/src/main/java/ani/dantotsu/aniyomi/core/preference/AndroidPreference.kt
rename to app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt
index 9eac6943..a62f16d7 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/core/preference/AndroidPreference.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.core.preference
+package eu.kanade.tachiyomi.core.preference
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
+import tachiyomi.core.preference.Preference
sealed class AndroidPreference(
private val preferences: SharedPreferences,
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/core/preference/AndroidPreferenceStore.kt b/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt
similarity index 77%
rename from app/src/main/java/ani/dantotsu/aniyomi/core/preference/AndroidPreferenceStore.kt
rename to app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt
index 74bb4c2a..61c2e2e0 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/core/preference/AndroidPreferenceStore.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt
@@ -1,18 +1,20 @@
-package ani.dantotsu.aniyomi.core.preference
+package eu.kanade.tachiyomi.core.preference
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
-import ani.dantotsu.aniyomi.core.preference.AndroidPreference.BooleanPrimitive
-import ani.dantotsu.aniyomi.core.preference.AndroidPreference.FloatPrimitive
-import ani.dantotsu.aniyomi.core.preference.AndroidPreference.IntPrimitive
-import ani.dantotsu.aniyomi.core.preference.AndroidPreference.LongPrimitive
-import ani.dantotsu.aniyomi.core.preference.AndroidPreference.StringPrimitive
-import ani.dantotsu.aniyomi.core.preference.AndroidPreference.StringSetPrimitive
-import ani.dantotsu.aniyomi.core.preference.AndroidPreference.Object
+import eu.kanade.tachiyomi.core.preference.AndroidPreference.BooleanPrimitive
+import eu.kanade.tachiyomi.core.preference.AndroidPreference.FloatPrimitive
+import eu.kanade.tachiyomi.core.preference.AndroidPreference.IntPrimitive
+import eu.kanade.tachiyomi.core.preference.AndroidPreference.LongPrimitive
+import eu.kanade.tachiyomi.core.preference.AndroidPreference.Object
+import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringPrimitive
+import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringSetPrimitive
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
+import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.PreferenceStore
class AndroidPreferenceStore(
context: Context,
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/data/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
similarity index 92%
rename from app/src/main/java/ani/dantotsu/aniyomi/data/NotificationReceiver.kt
rename to app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
index c5c30d2d..e4e9f0e6 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/data/NotificationReceiver.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
@@ -1,11 +1,11 @@
-package ani.dantotsu.aniyomi.data
+package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ani.dantotsu.MainActivity
-import ani.dantotsu.aniyomi.core.Constants
+import eu.kanade.tachiyomi.core.Constants
/**
* Global [BroadcastReceiver] that runs on UI thread
* Pending Broadcasts should be made from here.
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/data/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/data/Notifications.kt
rename to app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
index d9d6609f..6696d3cf 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/data/Notifications.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
@@ -1,12 +1,12 @@
-package ani.dantotsu.aniyomi.data
+package eu.kanade.tachiyomi.data.notification
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
-import ani.dantotsu.aniyomi.util.system.buildNotificationChannel
-import ani.dantotsu.aniyomi.util.system.buildNotificationChannelGroup
+import eu.kanade.tachiyomi.util.system.buildNotificationChannel
+import eu.kanade.tachiyomi.util.system.buildNotificationChannelGroup
/**
* Class to manage the basic information of all the notifications used in the app.
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/extension/ExtensionUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateNotifier.kt
similarity index 79%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/extension/ExtensionUpdateNotifier.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateNotifier.kt
index 3f7f7540..84acd4b3 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/extension/ExtensionUpdateNotifier.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateNotifier.kt
@@ -1,11 +1,11 @@
-package ani.dantotsu.aniyomi.util.extension
+package eu.kanade.tachiyomi.extension
import android.content.Context
import androidx.core.app.NotificationCompat
import ani.dantotsu.R
-import ani.dantotsu.aniyomi.data.NotificationReceiver
-import ani.dantotsu.aniyomi.data.Notifications
-import ani.dantotsu.aniyomi.util.system.notify
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.util.system.notify
class ExtensionUpdateNotifier(private val context: Context) {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/extension/InstallStep.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/InstallStep.kt
similarity index 81%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/extension/InstallStep.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/InstallStep.kt
index 3fd33dbb..3fba10fe 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/extension/InstallStep.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/InstallStep.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.extension
+package eu.kanade.tachiyomi.extension
enum class InstallStep {
Idle, Pending, Downloading, Installing, Installed, Error;
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/AnimeExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt
similarity index 82%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/AnimeExtensionManager.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt
index c77120f9..68bcfd88 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/AnimeExtensionManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt
@@ -1,27 +1,30 @@
-package ani.dantotsu.aniyomi.anime
+package eu.kanade.tachiyomi.extension.anime
import android.content.Context
import android.graphics.drawable.Drawable
-import ani.dantotsu.aniyomi.domain.source.anime.model.AnimeSourceData
-import ani.dantotsu.aniyomi.util.extension.InstallStep
-import ani.dantotsu.aniyomi.util.launchNow
-import ani.dantotsu.aniyomi.anime.api.AnimeExtensionGithubApi
-import ani.dantotsu.aniyomi.anime.model.AnimeExtension
-import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult
-import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstallReceiver
-import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstaller
-import ani.dantotsu.aniyomi.anime.util.AnimeExtensionLoader
-import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource
-//import eu.kanade.tachiyomi.util.preference.plusAssign
-import ani.dantotsu.aniyomi.util.toast
+import eu.kanade.domain.source.service.SourcePreferences
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
+import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
+import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
+import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
+import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallReceiver
+import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller
+import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
+import eu.kanade.tachiyomi.util.preference.plusAssign
+import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import logcat.LogPriority
import rx.Observable
-import ani.dantotsu.aniyomi.util.logcat
-import ani.dantotsu.aniyomi.util.withUIContext
-import ani.dantotsu.logger
+import tachiyomi.core.util.lang.launchNow
+import tachiyomi.core.util.lang.withUIContext
+import tachiyomi.core.util.system.logcat
+import tachiyomi.domain.source.anime.model.AnimeSourceData
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.Locale
/**
* The manager of anime extensions installed as another apk which extend the available sources. It handles
@@ -35,6 +38,7 @@ import ani.dantotsu.logger
*/
class AnimeExtensionManager(
private val context: Context,
+ private val preferences: SourcePreferences = Injekt.get(),
) {
var isInitialized = false
@@ -55,7 +59,7 @@ class AnimeExtensionManager(
private val _installedAnimeExtensionsFlow = MutableStateFlow(emptyList())
val installedExtensionsFlow = _installedAnimeExtensionsFlow.asStateFlow()
- private var subLanguagesEnabledOnFirstRun = false
+ private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedAnimeExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
@@ -92,41 +96,6 @@ class AnimeExtensionManager(
*/
private fun initAnimeExtensions() {
val animeextensions = AnimeExtensionLoader.loadExtensions(context)
- logcat { "Loaded ${animeextensions.size} anime extensions" }
- for (result in animeextensions) {
- when (result) {
- is AnimeLoadResult.Success -> {
- logcat { "Loaded: ${result.extension.pkgName}" }
- for(source in result.extension.sources) {
- logcat { "Loaded: ${source.name}" }
- }
- val sc = result.extension.sources.first()
- if (sc is AnimeCatalogueSource) {
- //val res = sc.fetchSearchAnime(1, "spy x family", AnimeFilterList()).toBlocking().first()
- /*val newScope = CoroutineScope(Dispatchers.IO)
- newScope.launch {
- println("fetching popular anime")
- try {
- val res = sc.fetchPopularAnime(1).toBlocking().first()
- println("res111: $res")
- }
- catch (e: Exception) {
- println("Exception111: $e")
- }
-
- }*/
-
-
- }else{
- println("${sc.name} is not AnimeCatalogueSource")
- }
- }
-
- else -> {
- logcat(LogPriority.ERROR) { "Error loading anime extension: $result." }
- }
- }
- }
_installedAnimeExtensionsFlow.value = animeextensions
.filterIsInstance()
@@ -147,14 +116,13 @@ class AnimeExtensionManager(
api.findExtensions()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
- withUIContext { context.toast("Could not update anime extensions") }
+ withUIContext { context.toast("Failed to get extensions list") }
emptyList()
}
enableAdditionalSubLanguages(extensions)
_availableAnimeExtensionsFlow.value = extensions
- println("AnimeExtensions: $extensions")
updatedInstalledAnimeExtensionsStatuses(extensions)
setupAvailableAnimeExtensionsSourcesDataMap(extensions)
}
@@ -174,7 +142,7 @@ class AnimeExtensionManager(
}
// Use the source lang as some aren't present on the animeextension level.
- /*val availableLanguages = animeextensions
+ val availableLanguages = animeextensions
.flatMap(AnimeExtension.Available::sources)
.distinctBy(AvailableAnimeSources::lang)
.map(AvailableAnimeSources::lang)
@@ -185,7 +153,7 @@ class AnimeExtensionManager(
it != deviceLanguage && it.startsWith(deviceLanguage)
}
- preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)*/
+ preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)
subLanguagesEnabledOnFirstRun = true
}
@@ -196,7 +164,7 @@ class AnimeExtensionManager(
*/
private fun updatedInstalledAnimeExtensionsStatuses(availableAnimeExtensions: List) {
if (availableAnimeExtensions.isEmpty()) {
- //preferences.animeExtensionUpdatesCount().set(0)
+ preferences.animeExtensionUpdatesCount().set(0)
return
}
@@ -286,7 +254,7 @@ class AnimeExtensionManager(
if (signature !in untrustedSignatures) return
AnimeExtensionLoader.trustedSignatures += signature
- //preferences.trustedSignatures() += signature
+ preferences.trustedSignatures() += signature
val nowTrustedAnimeExtensions = _untrustedAnimeExtensionsFlow.value.filter { it.signatureHash == signature }
_untrustedAnimeExtensionsFlow.value -= nowTrustedAnimeExtensions
@@ -392,6 +360,6 @@ class AnimeExtensionManager(
}
private fun updatePendingUpdatesCount() {
- //preferences.animeExtensionUpdatesCount().set(_installedAnimeExtensionsFlow.value.count { it.hasUpdate })
+ preferences.animeExtensionUpdatesCount().set(_installedAnimeExtensionsFlow.value.count { it.hasUpdate })
}
}
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/api/AnimeExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionGithubApi.kt
similarity index 84%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/api/AnimeExtensionGithubApi.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionGithubApi.kt
index 83925bcf..8a6ffbfa 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/api/AnimeExtensionGithubApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionGithubApi.kt
@@ -1,13 +1,12 @@
-package ani.dantotsu.aniyomi.anime.api
+package eu.kanade.tachiyomi.extension.anime.api
import android.content.Context
-import ani.dantotsu.aniyomi.util.extension.ExtensionUpdateNotifier
-import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
-import ani.dantotsu.aniyomi.anime.model.AnimeExtension
-import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult
-import ani.dantotsu.aniyomi.anime.model.AvailableAnimeSources
-import ani.dantotsu.aniyomi.anime.util.AnimeExtensionLoader
-import ani.dantotsu.aniyomi.core.preference.PreferenceStore
+import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
+import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
+import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
+import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
+import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
+import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
@@ -15,11 +14,13 @@ import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import logcat.LogPriority
-//import ani.dantotsu.aniyomi.core.preference.Preference
-//import ani.dantotsu.aniyomi.core.preference.PreferenceStore
-import ani.dantotsu.aniyomi.util.withIOContext
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.core.util.lang.withIOContext
+import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
+import java.util.Date
+import kotlin.time.Duration.Companion.days
internal class AnimeExtensionGithubApi {
@@ -28,10 +29,9 @@ internal class AnimeExtensionGithubApi {
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val json: Json by injectLazy()
- //private val lastExtCheck: Preference by lazy {
- // preferenceStore.getLong("last_ext_check", 0)
- //}
- private val lastExtCheck: Long = 0
+ private val lastExtCheck: Preference by lazy {
+ preferenceStore.getLong("last_ext_check", 0)
+ }
private var requiresFallbackSource = false
@@ -75,14 +75,14 @@ internal class AnimeExtensionGithubApi {
suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List? {
// Limit checks to once a day at most
- //if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) {
- // return null
- //}
+ if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) {
+ return null
+ }
val extensions = if (fromAvailableExtensionList) {
animeExtensionManager.availableExtensionsFlow.value
} else {
- findExtensions().also { }//lastExtCheck.set(Date().time) }
+ findExtensions().also { lastExtCheck.set(Date().time) }
}
val installedExtensions = AnimeExtensionLoader.loadExtensions(context)
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/installer/InstallerAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/InstallerAnime.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/installer/InstallerAnime.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/InstallerAnime.kt
index e41d5261..eaa6c45e 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/installer/InstallerAnime.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/InstallerAnime.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.anime.installer
+package eu.kanade.tachiyomi.extension.anime.installer
import android.app.Service
import android.content.BroadcastReceiver
@@ -8,8 +8,8 @@ import android.content.IntentFilter
import android.net.Uri
import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import ani.dantotsu.aniyomi.util.extension.InstallStep
-import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import uy.kohesive.injekt.injectLazy
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/installer/PackageInstallerInstallerAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/PackageInstallerInstallerAnime.kt
similarity index 93%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/installer/PackageInstallerInstallerAnime.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/PackageInstallerInstallerAnime.kt
index 9141129a..fb6bfb11 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/installer/PackageInstallerInstallerAnime.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/PackageInstallerInstallerAnime.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.anime.installer
+package eu.kanade.tachiyomi.extension.anime.installer
import android.app.PendingIntent
import android.app.Service
@@ -8,12 +8,12 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
-import ani.dantotsu.aniyomi.util.extension.InstallStep
-import ani.dantotsu.aniyomi.util.lang.use
-import ani.dantotsu.aniyomi.util.system.getParcelableExtraCompat
-import ani.dantotsu.aniyomi.util.system.getUriSize
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.util.lang.use
+import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
+import eu.kanade.tachiyomi.util.system.getUriSize
import logcat.LogPriority
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.util.system.logcat
class PackageInstallerInstallerAnime(private val service: Service) : InstallerAnime(service) {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/model/AnimeExtension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeExtension.kt
similarity index 93%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/model/AnimeExtension.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeExtension.kt
index a0446979..4f552879 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/model/AnimeExtension.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeExtension.kt
@@ -1,8 +1,8 @@
-package ani.dantotsu.aniyomi.anime.model
+package eu.kanade.tachiyomi.extension.anime.model
import android.graphics.drawable.Drawable
-import ani.dantotsu.aniyomi.animesource.AnimeSource
-import ani.dantotsu.aniyomi.domain.source.anime.model.AnimeSourceData
+import eu.kanade.tachiyomi.animesource.AnimeSource
+import tachiyomi.domain.source.anime.model.AnimeSourceData
sealed class AnimeExtension {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/model/AnimeLoadResult.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeLoadResult.kt
similarity index 82%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/model/AnimeLoadResult.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeLoadResult.kt
index ad1090bd..0f646c1f 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/model/AnimeLoadResult.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeLoadResult.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.anime.model
+package eu.kanade.tachiyomi.extension.anime.model
sealed class AnimeLoadResult {
class Success(val extension: AnimeExtension.Installed) : AnimeLoadResult()
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallActivity.kt
similarity index 90%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallActivity.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallActivity.kt
index 26a50483..692f52bb 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallActivity.kt
@@ -1,12 +1,12 @@
-package ani.dantotsu.aniyomi.anime.util
+package eu.kanade.tachiyomi.extension.anime.util
import android.app.Activity
import android.content.Intent
import android.os.Bundle
-import ani.dantotsu.aniyomi.util.extension.InstallStep
-import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
-import ani.dantotsu.aniyomi.util.system.hasMiuiPackageInstaller
-import ani.dantotsu.aniyomi.util.toast
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
+import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
+import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallReceiver.kt
similarity index 94%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallReceiver.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallReceiver.kt
index 66c3d72d..3f1ef0ea 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallReceiver.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallReceiver.kt
@@ -1,18 +1,18 @@
-package ani.dantotsu.aniyomi.anime.util
+package eu.kanade.tachiyomi.extension.anime.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import ani.dantotsu.aniyomi.anime.model.AnimeExtension
-import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult
+import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
+import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import logcat.LogPriority
-import ani.dantotsu.aniyomi.util.launchNow
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.util.lang.launchNow
+import tachiyomi.core.util.system.logcat
/**
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallService.kt
similarity index 80%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallService.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallService.kt
index 0f9d4584..62ad05c4 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstallService.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstallService.kt
@@ -1,20 +1,20 @@
-package ani.dantotsu.aniyomi.anime.util
+package eu.kanade.tachiyomi.extension.anime.util
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.IBinder
+import eu.kanade.domain.base.BasePreferences
import ani.dantotsu.R
-import ani.dantotsu.aniyomi.domain.base.BasePreferences
-import ani.dantotsu.aniyomi.data.Notifications
-import ani.dantotsu.aniyomi.anime.installer.InstallerAnime
-import ani.dantotsu.aniyomi.anime.installer.PackageInstallerInstallerAnime
-import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
-import ani.dantotsu.aniyomi.util.system.getSerializableExtraCompat
-import ani.dantotsu.aniyomi.util.system.notificationBuilder
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
+import eu.kanade.tachiyomi.extension.anime.installer.PackageInstallerInstallerAnime
+import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
+import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
+import eu.kanade.tachiyomi.util.system.notificationBuilder
import logcat.LogPriority
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.util.system.logcat
class AnimeExtensionInstallService : Service() {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstaller.kt
similarity index 96%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstaller.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstaller.kt
index bc6372dd..fc555129 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionInstaller.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionInstaller.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.anime.util
+package eu.kanade.tachiyomi.extension.anime.util
import android.app.DownloadManager
import android.content.BroadcastReceiver
@@ -11,15 +11,15 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay
-import ani.dantotsu.aniyomi.util.extension.InstallStep
-import ani.dantotsu.aniyomi.anime.installer.InstallerAnime
-import ani.dantotsu.aniyomi.anime.model.AnimeExtension
-import ani.dantotsu.aniyomi.domain.base.BasePreferences
-import ani.dantotsu.aniyomi.util.storage.getUriCompat
+import eu.kanade.domain.base.BasePreferences
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
+import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
+import eu.kanade.tachiyomi.util.storage.getUriCompat
import logcat.LogPriority
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt
similarity index 93%
rename from app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionLoader.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt
index 1356860b..745366ed 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/anime/util/AnimeExtensionLoader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.anime.util
+package eu.kanade.tachiyomi.extension.anime.util
import android.annotation.SuppressLint
import android.content.Context
@@ -7,18 +7,18 @@ import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import dalvik.system.PathClassLoader
-import ani.dantotsu.aniyomi.domain.source.service.SourcePreferences
-import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource
-import ani.dantotsu.aniyomi.animesource.AnimeSource
-import ani.dantotsu.aniyomi.animesource.AnimeSourceFactory
-import ani.dantotsu.aniyomi.anime.model.AnimeExtension
-import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult
-import ani.dantotsu.aniyomi.util.lang.Hash
-import ani.dantotsu.aniyomi.util.system.getApplicationIcon
+import eu.kanade.domain.source.service.SourcePreferences
+import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
+import eu.kanade.tachiyomi.animesource.AnimeSource
+import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
+import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
+import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
+import eu.kanade.tachiyomi.util.lang.Hash
+import eu.kanade.tachiyomi.util.system.getApplicationIcon
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
/**
@@ -130,7 +130,7 @@ internal object AnimeExtensionLoader {
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
logcat(LogPriority.WARN) {
"Lib version is $libVersion, while only versions " +
- "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
+ "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
}
return AnimeLoadResult.Error
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt
new file mode 100644
index 00000000..549ba550
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt
@@ -0,0 +1,365 @@
+package eu.kanade.tachiyomi.extension.manga
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import eu.kanade.domain.source.service.SourcePreferences
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
+import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
+import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
+import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
+import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver
+import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller
+import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
+import eu.kanade.tachiyomi.util.preference.plusAssign
+import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import logcat.LogPriority
+import rx.Observable
+import tachiyomi.core.util.lang.launchNow
+import tachiyomi.core.util.lang.withUIContext
+import tachiyomi.core.util.system.logcat
+import tachiyomi.domain.source.manga.model.MangaSourceData
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.Locale
+
+/**
+ * The manager of extensions installed as another apk which extend the available sources. It handles
+ * the retrieval of remotely available extensions as well as installing, updating and removing them.
+ * To avoid malicious distribution, every extension must be signed and it will only be loaded if its
+ * signature is trusted, otherwise the user will be prompted with a warning to trust it before being
+ * loaded.
+ *
+ * @param context The application context.
+ * @param preferences The application preferences.
+ */
+class MangaExtensionManager(
+ private val context: Context,
+ private val preferences: SourcePreferences = Injekt.get(),
+) {
+
+ var isInitialized = false
+ private set
+
+ /**
+ * API where all the available extensions can be found.
+ */
+ private val api = MangaExtensionGithubApi()
+
+ /**
+ * The installer which installs, updates and uninstalls the extensions.
+ */
+ private val installer by lazy { MangaExtensionInstaller(context) }
+
+ private val iconMap = mutableMapOf()
+
+ private val _installedExtensionsFlow = MutableStateFlow(emptyList())
+ val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow()
+
+ private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
+
+ fun getAppIconForSource(sourceId: Long): Drawable? {
+ val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
+ if (pkgName != null) {
+ return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
+ }
+ return null
+ }
+
+ private val _availableExtensionsFlow = MutableStateFlow(emptyList())
+ val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow()
+
+ private var availableExtensionsSourcesData: Map = emptyMap()
+
+ private fun setupAvailableExtensionsSourcesDataMap(extensions: List) {
+ if (extensions.isEmpty()) return
+ availableExtensionsSourcesData = extensions
+ .flatMap { ext -> ext.sources.map { it.toSourceData() } }
+ .associateBy { it.id }
+ }
+
+ fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
+
+ private val _untrustedExtensionsFlow = MutableStateFlow(emptyList())
+ val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow()
+
+ init {
+ initExtensions()
+ MangaExtensionInstallReceiver(InstallationListener()).register(context)
+ }
+
+ /**
+ * Loads and registers the installed extensions.
+ */
+ private fun initExtensions() {
+ val extensions = MangaExtensionLoader.loadMangaExtensions(context)
+
+ _installedExtensionsFlow.value = extensions
+ .filterIsInstance()
+ .map { it.extension }
+
+ _untrustedExtensionsFlow.value = extensions
+ .filterIsInstance()
+ .map { it.extension }
+
+ isInitialized = true
+ }
+
+ /**
+ * Finds the available extensions in the [api] and updates [availableExtensions].
+ */
+ suspend fun findAvailableExtensions() {
+ val extensions: List = try {
+ api.findExtensions()
+ } catch (e: Exception) {
+ logcat(LogPriority.ERROR, e)
+ withUIContext { context.toast("Failed to get manga extensions") }
+ emptyList()
+ }
+
+ enableAdditionalSubLanguages(extensions)
+
+ _availableExtensionsFlow.value = extensions
+ updatedInstalledExtensionsStatuses(extensions)
+ setupAvailableExtensionsSourcesDataMap(extensions)
+ }
+
+ /**
+ * Enables the additional sub-languages in the app first run. This addresses
+ * the issue where users still need to enable some specific languages even when
+ * the device language is inside that major group. As an example, if a user
+ * has a zh device language, the app will also enable zh-Hans and zh-Hant.
+ *
+ * If the user have already changed the enabledLanguages preference value once,
+ * the new languages will not be added to respect the user enabled choices.
+ */
+ private fun enableAdditionalSubLanguages(extensions: List) {
+ if (subLanguagesEnabledOnFirstRun || extensions.isEmpty()) {
+ return
+ }
+
+ // Use the source lang as some aren't present on the extension level.
+ val availableLanguages = extensions
+ .flatMap(MangaExtension.Available::sources)
+ .distinctBy(AvailableMangaSources::lang)
+ .map(AvailableMangaSources::lang)
+
+ val deviceLanguage = Locale.getDefault().language
+ val defaultLanguages = preferences.enabledLanguages().defaultValue()
+ val languagesToEnable = availableLanguages.filter {
+ it != deviceLanguage && it.startsWith(deviceLanguage)
+ }
+
+ preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)
+ subLanguagesEnabledOnFirstRun = true
+ }
+
+ /**
+ * Sets the update field of the installed extensions with the given [availableExtensions].
+ *
+ * @param availableExtensions The list of extensions given by the [api].
+ */
+ private fun updatedInstalledExtensionsStatuses(availableExtensions: List) {
+ if (availableExtensions.isEmpty()) {
+ preferences.mangaExtensionUpdatesCount().set(0)
+ return
+ }
+
+ val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
+ var changed = false
+
+ for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
+ val pkgName = installedExt.pkgName
+ val availableExt = availableExtensions.find { it.pkgName == pkgName }
+
+ if (!installedExt.isUnofficial && availableExt == null && !installedExt.isObsolete) {
+ mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
+ changed = true
+ } else if (availableExt != null) {
+ val hasUpdate = installedExt.updateExists(availableExt)
+
+ if (installedExt.hasUpdate != hasUpdate) {
+ mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
+ changed = true
+ }
+ }
+ }
+ if (changed) {
+ _installedExtensionsFlow.value = mutInstalledExtensions
+ }
+ updatePendingUpdatesCount()
+ }
+
+ /**
+ * Returns an observable of the installation process for the given extension. It will complete
+ * once the extension is installed or throws an error. The process will be canceled if
+ * unsubscribed before its completion.
+ *
+ * @param extension The extension to be installed.
+ */
+ fun installExtension(extension: MangaExtension.Available): Observable {
+ return installer.downloadAndInstall(api.getApkUrl(extension), extension)
+ }
+
+ /**
+ * Returns an observable of the installation process for the given extension. It will complete
+ * once the extension is updated or throws an error. The process will be canceled if
+ * unsubscribed before its completion.
+ *
+ * @param extension The extension to be updated.
+ */
+ fun updateExtension(extension: MangaExtension.Installed): Observable {
+ val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
+ ?: return Observable.empty()
+ return installExtension(availableExt)
+ }
+
+ fun cancelInstallUpdateExtension(extension: MangaExtension) {
+ installer.cancelInstall(extension.pkgName)
+ }
+
+ /**
+ * Sets to "installing" status of an extension installation.
+ *
+ * @param downloadId The id of the download.
+ */
+ fun setInstalling(downloadId: Long) {
+ installer.updateInstallStep(downloadId, InstallStep.Installing)
+ }
+
+ fun updateInstallStep(downloadId: Long, step: InstallStep) {
+ installer.updateInstallStep(downloadId, step)
+ }
+
+ /**
+ * Uninstalls the extension that matches the given package name.
+ *
+ * @param pkgName The package name of the application to uninstall.
+ */
+ fun uninstallExtension(pkgName: String) {
+ installer.uninstallApk(pkgName)
+ }
+
+ /**
+ * Adds the given signature to the list of trusted signatures. It also loads in background the
+ * extensions that match this signature.
+ *
+ * @param signature The signature to whitelist.
+ */
+ fun trustSignature(signature: String) {
+ val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
+ if (signature !in untrustedSignatures) return
+
+ MangaExtensionLoader.trustedSignatures += signature
+ preferences.trustedSignatures() += signature
+
+ val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
+ _untrustedExtensionsFlow.value -= nowTrustedExtensions
+
+ val ctx = context
+ launchNow {
+ nowTrustedExtensions
+ .map { extension ->
+ async { MangaExtensionLoader.loadMangaExtensionFromPkgName(ctx, extension.pkgName) }
+ }
+ .map { it.await() }
+ .forEach { result ->
+ if (result is MangaLoadResult.Success) {
+ registerNewExtension(result.extension)
+ }
+ }
+ }
+ }
+
+ /**
+ * Registers the given extension in this and the source managers.
+ *
+ * @param extension The extension to be registered.
+ */
+ private fun registerNewExtension(extension: MangaExtension.Installed) {
+ _installedExtensionsFlow.value += extension
+ }
+
+ /**
+ * Registers the given updated extension in this and the source managers previously removing
+ * the outdated ones.
+ *
+ * @param extension The extension to be registered.
+ */
+ private fun registerUpdatedExtension(extension: MangaExtension.Installed) {
+ val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
+ val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
+ if (oldExtension != null) {
+ mutInstalledExtensions -= oldExtension
+ }
+ mutInstalledExtensions += extension
+ _installedExtensionsFlow.value = mutInstalledExtensions
+ }
+
+ /**
+ * Unregisters the extension in this and the source managers given its package name. Note this
+ * method is called for every uninstalled application in the system.
+ *
+ * @param pkgName The package name of the uninstalled application.
+ */
+ private fun unregisterExtension(pkgName: String) {
+ val installedExtension = _installedExtensionsFlow.value.find { it.pkgName == pkgName }
+ if (installedExtension != null) {
+ _installedExtensionsFlow.value -= installedExtension
+ }
+ val untrustedExtension = _untrustedExtensionsFlow.value.find { it.pkgName == pkgName }
+ if (untrustedExtension != null) {
+ _untrustedExtensionsFlow.value -= untrustedExtension
+ }
+ }
+
+ /**
+ * Listener which receives events of the extensions being installed, updated or removed.
+ */
+ private inner class InstallationListener : MangaExtensionInstallReceiver.Listener {
+
+ override fun onExtensionInstalled(extension: MangaExtension.Installed) {
+ registerNewExtension(extension.withUpdateCheck())
+ updatePendingUpdatesCount()
+ }
+
+ override fun onExtensionUpdated(extension: MangaExtension.Installed) {
+ registerUpdatedExtension(extension.withUpdateCheck())
+ updatePendingUpdatesCount()
+ }
+
+ override fun onExtensionUntrusted(extension: MangaExtension.Untrusted) {
+ _untrustedExtensionsFlow.value += extension
+ }
+
+ override fun onPackageUninstalled(pkgName: String) {
+ unregisterExtension(pkgName)
+ updatePendingUpdatesCount()
+ }
+ }
+
+ /**
+ * Extension method to set the update field of an installed extension.
+ */
+ private fun MangaExtension.Installed.withUpdateCheck(): MangaExtension.Installed {
+ return if (updateExists()) {
+ copy(hasUpdate = true)
+ } else {
+ this
+ }
+ }
+
+ private fun MangaExtension.Installed.updateExists(availableExtension: MangaExtension.Available? = null): Boolean {
+ val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
+ if (isUnofficial || availableExt == null) return false
+
+ return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
+ }
+
+ private fun updatePendingUpdatesCount() {
+ preferences.mangaExtensionUpdatesCount().set(_installedExtensionsFlow.value.count { it.hasUpdate })
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/api/MangaExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/api/MangaExtensionGithubApi.kt
new file mode 100644
index 00000000..dd4540c2
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/api/MangaExtensionGithubApi.kt
@@ -0,0 +1,186 @@
+package eu.kanade.tachiyomi.extension.manga.api
+
+import android.content.Context
+import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
+import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
+import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
+import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
+import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
+import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.network.awaitSuccess
+import eu.kanade.tachiyomi.network.parseAs
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import logcat.LogPriority
+import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.core.util.lang.withIOContext
+import tachiyomi.core.util.system.logcat
+import uy.kohesive.injekt.injectLazy
+import java.util.Date
+import kotlin.time.Duration.Companion.days
+
+internal class MangaExtensionGithubApi {
+
+ private val networkService: NetworkHelper by injectLazy()
+ private val preferenceStore: PreferenceStore by injectLazy()
+ private val extensionManager: MangaExtensionManager by injectLazy()
+ private val json: Json by injectLazy()
+
+ private val lastExtCheck: Preference by lazy {
+ preferenceStore.getLong("last_ext_check", 0)
+ }
+
+ private var requiresFallbackSource = false
+
+ suspend fun findExtensions(): List {
+ return withIOContext {
+ val githubResponse = if (requiresFallbackSource) {
+ null
+ } else {
+ try {
+ networkService.client
+ .newCall(GET("${REPO_URL_PREFIX}index.min.json"))
+ .awaitSuccess()
+ } catch (e: Throwable) {
+ logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
+ requiresFallbackSource = true
+ null
+ }
+ }
+
+ val response = githubResponse ?: run {
+ networkService.client
+ .newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
+ .awaitSuccess()
+ }
+
+ val extensions = with(json) {
+ response
+ .parseAs>()
+ .toExtensions()
+ }
+
+ // Sanity check - a small number of extensions probably means something broke
+ // with the repo generator
+ if (extensions.size < 100) {
+ throw Exception()
+ }
+
+ extensions
+ }
+ }
+
+ suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List? {
+ // Limit checks to once a day at most
+ if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) {
+ return null
+ }
+
+ val extensions = if (fromAvailableExtensionList) {
+ extensionManager.availableExtensionsFlow.value
+ } else {
+ findExtensions().also { lastExtCheck.set(Date().time) }
+ }
+
+ val installedExtensions = MangaExtensionLoader.loadMangaExtensions(context)
+ .filterIsInstance()
+ .map { it.extension }
+
+ val extensionsWithUpdate = mutableListOf()
+ for (installedExt in installedExtensions) {
+ val pkgName = installedExt.pkgName
+ val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
+ val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
+ val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion
+ val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer || hasUpdatedLib)
+ if (hasUpdate) {
+ extensionsWithUpdate.add(installedExt)
+ }
+ }
+
+ if (extensionsWithUpdate.isNotEmpty()) {
+ ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name })
+ }
+
+ return extensionsWithUpdate
+ }
+
+ private fun List.toExtensions(): List {
+ return this
+ .filter {
+ val libVersion = it.extractLibVersion()
+ libVersion >= MangaExtensionLoader.LIB_VERSION_MIN && libVersion <= MangaExtensionLoader.LIB_VERSION_MAX
+ }
+ .map {
+ MangaExtension.Available(
+ name = it.name.substringAfter("Tachiyomi: "),
+ pkgName = it.pkg,
+ versionName = it.version,
+ versionCode = it.code,
+ libVersion = it.extractLibVersion(),
+ lang = it.lang,
+ isNsfw = it.nsfw == 1,
+ hasReadme = it.hasReadme == 1,
+ hasChangelog = it.hasChangelog == 1,
+ sources = it.sources?.toExtensionSources().orEmpty(),
+ apkName = it.apk,
+ iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
+ )
+ }
+ }
+
+ private fun List.toExtensionSources(): List {
+ return this.map {
+ AvailableMangaSources(
+ id = it.id,
+ lang = it.lang,
+ name = it.name,
+ baseUrl = it.baseUrl,
+ )
+ }
+ }
+
+ fun getApkUrl(extension: MangaExtension.Available): String {
+ return "${getUrlPrefix()}apk/${extension.apkName}"
+ }
+
+ private fun getUrlPrefix(): String {
+ return if (requiresFallbackSource) {
+ FALLBACK_REPO_URL_PREFIX
+ } else {
+ REPO_URL_PREFIX
+ }
+ }
+
+ private fun ExtensionJsonObject.extractLibVersion(): Double {
+ return version.substringBeforeLast('.').toDouble()
+ }
+}
+
+private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
+private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
+
+@Serializable
+private data class ExtensionJsonObject(
+ val name: String,
+ val pkg: String,
+ val apk: String,
+ val lang: String,
+ val code: Long,
+ val version: String,
+ val nsfw: Int,
+ val hasReadme: Int = 0,
+ val hasChangelog: Int = 0,
+ val sources: List?,
+)
+
+@Serializable
+private data class ExtensionSourceJsonObject(
+ val id: Long,
+ val lang: String,
+ val name: String,
+ val baseUrl: String,
+)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/InstallerManga.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/InstallerManga.kt
new file mode 100644
index 00000000..37c9edbb
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/InstallerManga.kt
@@ -0,0 +1,170 @@
+package eu.kanade.tachiyomi.extension.manga.installer
+
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
+import androidx.annotation.CallSuper
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
+import uy.kohesive.injekt.injectLazy
+import java.util.Collections
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Base implementation class for extension installer. To be used inside a foreground [Service].
+ */
+abstract class InstallerManga(private val service: Service) {
+
+ private val extensionManager: MangaExtensionManager by injectLazy()
+
+ private var waitingInstall = AtomicReference(null)
+ private val queue = Collections.synchronizedList(mutableListOf())
+
+ private val cancelReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
+ cancelQueue(downloadId)
+ }
+ }
+
+ /**
+ * Installer readiness. If false, queue check will not run.
+ *
+ * @see checkQueue
+ */
+ abstract var ready: Boolean
+
+ /**
+ * Add an item to install queue.
+ *
+ * @param downloadId Download ID as known by [MangaExtensionManager]
+ * @param uri Uri of APK to install
+ */
+ fun addToQueue(downloadId: Long, uri: Uri) {
+ queue.add(Entry(downloadId, uri))
+ checkQueue()
+ }
+
+ /**
+ * Proceeds to install the APK of this entry inside this method. Call [continueQueue]
+ * when the install process for this entry is finished to continue the queue.
+ *
+ * @param entry The [Entry] of item to process
+ * @see continueQueue
+ */
+ @CallSuper
+ open fun processEntry(entry: Entry) {
+ extensionManager.setInstalling(entry.downloadId)
+ }
+
+ /**
+ * Called before queue continues. Override this to handle when the removed entry is
+ * currently being processed.
+ *
+ * @return true if this entry can be removed from queue.
+ */
+ open fun cancelEntry(entry: Entry): Boolean {
+ return true
+ }
+
+ /**
+ * Tells the queue to continue processing the next entry and updates the install step
+ * of the completed entry ([waitingInstall]) to [MangaExtensionManager].
+ *
+ * @param resultStep new install step for the processed entry.
+ * @see waitingInstall
+ */
+ fun continueQueue(resultStep: InstallStep) {
+ val completedEntry = waitingInstall.getAndSet(null)
+ if (completedEntry != null) {
+ extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
+ checkQueue()
+ }
+ }
+
+ /**
+ * Checks the queue. The provided service will be stopped if the queue is empty.
+ * Will not be run when not ready.
+ *
+ * @see ready
+ */
+ fun checkQueue() {
+ if (!ready) {
+ return
+ }
+ if (queue.isEmpty()) {
+ service.stopSelf()
+ return
+ }
+ val nextEntry = queue.first()
+ if (waitingInstall.compareAndSet(null, nextEntry)) {
+ queue.removeFirst()
+ processEntry(nextEntry)
+ }
+ }
+
+ /**
+ * Call this method when the provided service is destroyed.
+ */
+ @CallSuper
+ open fun onDestroy() {
+ LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
+ queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
+ queue.clear()
+ waitingInstall.set(null)
+ }
+
+ protected fun getActiveEntry(): Entry? = waitingInstall.get()
+
+ /**
+ * Cancels queue for the provided download ID if exists.
+ *
+ * @param downloadId Download ID as known by [MangaExtensionManager]
+ */
+ private fun cancelQueue(downloadId: Long) {
+ val waitingInstall = this.waitingInstall.get()
+ val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
+ if (cancelEntry(toCancel)) {
+ queue.remove(toCancel)
+ if (waitingInstall == toCancel) {
+ // Currently processing removed entry, continue queue
+ this.waitingInstall.set(null)
+ checkQueue()
+ }
+ extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
+ }
+ }
+
+ /**
+ * Install item to queue.
+ *
+ * @param downloadId Download ID as known by [MangaExtensionManager]
+ * @param uri Uri of APK to install
+ */
+ data class Entry(val downloadId: Long, val uri: Uri)
+
+ init {
+ val filter = IntentFilter(ACTION_CANCEL_QUEUE)
+ LocalBroadcastManager.getInstance(service).registerReceiver(cancelReceiver, filter)
+ }
+
+ companion object {
+ private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
+ private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
+
+ /**
+ * Attempts to cancel the installation entry for the provided download ID.
+ *
+ * @param downloadId Download ID as known by [MangaExtensionManager]
+ */
+ fun cancelInstallQueue(context: Context, downloadId: Long) {
+ val intent = Intent(ACTION_CANCEL_QUEUE)
+ intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/PackageInstallerInstallerManga.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/PackageInstallerInstallerManga.kt
new file mode 100644
index 00000000..6eb719ee
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/PackageInstallerInstallerManga.kt
@@ -0,0 +1,107 @@
+package eu.kanade.tachiyomi.extension.manga.installer
+
+import android.app.PendingIntent
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageInstaller
+import android.os.Build
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.util.lang.use
+import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
+import eu.kanade.tachiyomi.util.system.getUriSize
+import logcat.LogPriority
+import tachiyomi.core.util.system.logcat
+
+class PackageInstallerInstallerManga(private val service: Service) : InstallerManga(service) {
+
+ private val packageInstaller = service.packageManager.packageInstaller
+
+ private val packageActionReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ val userAction = intent.getParcelableExtraCompat(Intent.EXTRA_INTENT)
+ if (userAction == null) {
+ logcat(LogPriority.ERROR) { "Fatal error for $intent" }
+ continueQueue(InstallStep.Error)
+ return
+ }
+ userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ service.startActivity(userAction)
+ }
+ PackageInstaller.STATUS_FAILURE_ABORTED -> {
+ continueQueue(InstallStep.Idle)
+ }
+ PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
+ else -> continueQueue(InstallStep.Error)
+ }
+ }
+ }
+
+ private var activeSession: Pair? = null
+
+ // Always ready
+ override var ready = true
+
+ override fun processEntry(entry: Entry) {
+ super.processEntry(entry)
+ activeSession = null
+ try {
+ val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
+ }
+ activeSession = entry to packageInstaller.createSession(installParams)
+ val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
+ installParams.setSize(fileSize)
+
+ val inputStream = service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
+ val session = packageInstaller.openSession(activeSession!!.second)
+ val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
+ session.use {
+ arrayOf(inputStream, outputStream).use {
+ inputStream.copyTo(outputStream)
+ session.fsync(outputStream)
+ }
+
+ val intentSender = PendingIntent.getBroadcast(
+ service,
+ activeSession!!.second,
+ Intent(INSTALL_ACTION),
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
+ ).intentSender
+ session.commit(intentSender)
+ }
+ } catch (e: Exception) {
+ logcat(LogPriority.ERROR) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
+ activeSession?.let { (_, sessionId) ->
+ packageInstaller.abandonSession(sessionId)
+ }
+ continueQueue(InstallStep.Error)
+ }
+ }
+
+ override fun cancelEntry(entry: Entry): Boolean {
+ activeSession?.let { (activeEntry, sessionId) ->
+ if (activeEntry == entry) {
+ packageInstaller.abandonSession(sessionId)
+ return false
+ }
+ }
+ return true
+ }
+
+ override fun onDestroy() {
+ service.unregisterReceiver(packageActionReceiver)
+ super.onDestroy()
+ }
+
+ init {
+ service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
+ }
+}
+
+private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/model/MangaExtension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/model/MangaExtension.kt
new file mode 100644
index 00000000..d0b6d448
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/model/MangaExtension.kt
@@ -0,0 +1,79 @@
+package eu.kanade.tachiyomi.extension.manga.model
+
+import android.graphics.drawable.Drawable
+import eu.kanade.tachiyomi.source.MangaSource
+import tachiyomi.domain.source.manga.model.MangaSourceData
+
+sealed class MangaExtension {
+
+ abstract val name: String
+ abstract val pkgName: String
+ abstract val versionName: String
+ abstract val versionCode: Long
+ abstract val libVersion: Double
+ abstract val lang: String?
+ abstract val isNsfw: Boolean
+ abstract val hasReadme: Boolean
+ abstract val hasChangelog: Boolean
+
+ data class Installed(
+ override val name: String,
+ override val pkgName: String,
+ override val versionName: String,
+ override val versionCode: Long,
+ override val libVersion: Double,
+ override val lang: String,
+ override val isNsfw: Boolean,
+ override val hasReadme: Boolean,
+ override val hasChangelog: Boolean,
+ val pkgFactory: String?,
+ val sources: List,
+ val icon: Drawable?,
+ val hasUpdate: Boolean = false,
+ val isObsolete: Boolean = false,
+ val isUnofficial: Boolean = false,
+ ) : MangaExtension()
+
+ data class Available(
+ override val name: String,
+ override val pkgName: String,
+ override val versionName: String,
+ override val versionCode: Long,
+ override val libVersion: Double,
+ override val lang: String,
+ override val isNsfw: Boolean,
+ override val hasReadme: Boolean,
+ override val hasChangelog: Boolean,
+ val sources: List,
+ val apkName: String,
+ val iconUrl: String,
+ ) : MangaExtension()
+
+ data class Untrusted(
+ override val name: String,
+ override val pkgName: String,
+ override val versionName: String,
+ override val versionCode: Long,
+ override val libVersion: Double,
+ val signatureHash: String,
+ override val lang: String? = null,
+ override val isNsfw: Boolean = false,
+ override val hasReadme: Boolean = false,
+ override val hasChangelog: Boolean = false,
+ ) : MangaExtension()
+}
+
+data class AvailableMangaSources(
+ val id: Long,
+ val lang: String,
+ val name: String,
+ val baseUrl: String,
+) {
+ fun toSourceData(): MangaSourceData {
+ return MangaSourceData(
+ id = this.id,
+ lang = this.lang,
+ name = this.name,
+ )
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/model/MangaLoadResult.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/model/MangaLoadResult.kt
new file mode 100644
index 00000000..fa5af78d
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/model/MangaLoadResult.kt
@@ -0,0 +1,7 @@
+package eu.kanade.tachiyomi.extension.manga.model
+
+sealed class MangaLoadResult {
+ class Success(val extension: MangaExtension.Installed) : MangaLoadResult()
+ class Untrusted(val extension: MangaExtension.Untrusted) : MangaLoadResult()
+ object Error : MangaLoadResult()
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallActivity.kt
new file mode 100644
index 00000000..743c8f27
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallActivity.kt
@@ -0,0 +1,78 @@
+package eu.kanade.tachiyomi.extension.manga.util
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
+import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
+import eu.kanade.tachiyomi.util.system.toast
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Activity used to install extensions, because we can only receive the result of the installation
+ * with [startActivityForResult], which we need to update the UI.
+ */
+class MangaExtensionInstallActivity : Activity() {
+
+ // MIUI package installer bug workaround
+ private var ignoreUntil = 0L
+ private var ignoreResult = false
+ private var hasIgnoredResult = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
+ .setDataAndType(intent.data, intent.type)
+ .putExtra(Intent.EXTRA_RETURN_RESULT, true)
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+
+ if (hasMiuiPackageInstaller) {
+ ignoreResult = true
+ ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
+ }
+
+ try {
+ startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
+ } catch (error: Exception) {
+ // Either install package can't be found (probably bots) or there's a security exception
+ // with the download manager. Nothing we can workaround.
+ toast(error.message)
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (ignoreResult && System.nanoTime() < ignoreUntil) {
+ hasIgnoredResult = true
+ return
+ }
+ if (requestCode == INSTALL_REQUEST_CODE) {
+ checkInstallationResult(resultCode)
+ }
+ finish()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (hasIgnoredResult) {
+ checkInstallationResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+
+ private fun checkInstallationResult(resultCode: Int) {
+ val downloadId = intent.extras!!.getLong(MangaExtensionInstaller.EXTRA_DOWNLOAD_ID)
+ val extensionManager = Injekt.get()
+ val newStep = when (resultCode) {
+ RESULT_OK -> InstallStep.Installed
+ RESULT_CANCELED -> InstallStep.Idle
+ else -> InstallStep.Error
+ }
+ extensionManager.updateInstallStep(downloadId, newStep)
+ }
+}
+
+private const val INSTALL_REQUEST_CODE = 500
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallReceiver.kt
new file mode 100644
index 00000000..17dac9b4
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallReceiver.kt
@@ -0,0 +1,130 @@
+package eu.kanade.tachiyomi.extension.manga.util
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
+import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import logcat.LogPriority
+import tachiyomi.core.util.lang.launchNow
+import tachiyomi.core.util.system.logcat
+
+/**
+ * Broadcast receiver that listens for the system's packages installed, updated or removed, and only
+ * notifies the given [listener] when the package is an extension.
+ *
+ * @param listener The listener that should be notified of extension installation events.
+ */
+internal class MangaExtensionInstallReceiver(private val listener: Listener) :
+ BroadcastReceiver() {
+
+ /**
+ * Registers this broadcast receiver
+ */
+ fun register(context: Context) {
+ context.registerReceiver(this, filter)
+ }
+
+ /**
+ * Returns the intent filter this receiver should subscribe to.
+ */
+ private val filter
+ get() = IntentFilter().apply {
+ addAction(Intent.ACTION_PACKAGE_ADDED)
+ addAction(Intent.ACTION_PACKAGE_REPLACED)
+ addAction(Intent.ACTION_PACKAGE_REMOVED)
+ addDataScheme("package")
+ }
+
+ /**
+ * 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.
+ */
+ override fun onReceive(context: Context, intent: Intent?) {
+ if (intent == null) return
+
+ when (intent.action) {
+ Intent.ACTION_PACKAGE_ADDED -> {
+ if (isReplacing(intent)) return
+
+ launchNow {
+ when (val result = getExtensionFromIntent(context, intent)) {
+ is MangaLoadResult.Success -> listener.onExtensionInstalled(result.extension)
+
+ is MangaLoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
+ else -> {}
+ }
+ }
+ }
+ Intent.ACTION_PACKAGE_REPLACED -> {
+ launchNow {
+ when (val result = getExtensionFromIntent(context, intent)) {
+ is MangaLoadResult.Success -> listener.onExtensionUpdated(result.extension)
+ // Not needed as a package can't be upgraded if the signature is different
+ // is LoadResult.Untrusted -> {}
+ else -> {}
+ }
+ }
+ }
+ Intent.ACTION_PACKAGE_REMOVED -> {
+ if (isReplacing(intent)) return
+
+ val pkgName = getPackageNameFromIntent(intent)
+ if (pkgName != null) {
+ listener.onPackageUninstalled(pkgName)
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if this package is performing an update.
+ *
+ * @param intent The intent that triggered the event.
+ */
+ private fun isReplacing(intent: Intent): Boolean {
+ return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
+ }
+
+ /**
+ * Returns the extension triggered by the given intent.
+ *
+ * @param context The application context.
+ * @param intent The intent containing the package name of the extension.
+ */
+ private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult {
+ val pkgName = getPackageNameFromIntent(intent)
+ if (pkgName == null) {
+ logcat(LogPriority.WARN) { "Package name not found" }
+ return MangaLoadResult.Error
+ }
+ return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
+ MangaExtensionLoader.loadMangaExtensionFromPkgName(
+ context,
+ pkgName,
+ )
+ }.await()
+ }
+
+ /**
+ * Returns the package name of the installed, updated or removed application.
+ */
+ private fun getPackageNameFromIntent(intent: Intent?): String? {
+ return intent?.data?.encodedSchemeSpecificPart ?: return null
+ }
+
+ /**
+ * Listener that receives extension installation events.
+ */
+ interface Listener {
+ fun onExtensionInstalled(extension: MangaExtension.Installed)
+ fun onExtensionUpdated(extension: MangaExtension.Installed)
+ fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
+ fun onPackageUninstalled(pkgName: String)
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallService.kt
new file mode 100644
index 00000000..e4f01c2b
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstallService.kt
@@ -0,0 +1,82 @@
+package eu.kanade.tachiyomi.extension.manga.util
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.IBinder
+import ani.dantotsu.R
+import eu.kanade.domain.base.BasePreferences
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
+import eu.kanade.tachiyomi.extension.manga.installer.PackageInstallerInstallerManga
+import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
+import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
+import eu.kanade.tachiyomi.util.system.notificationBuilder
+import logcat.LogPriority
+import tachiyomi.core.util.system.logcat
+
+class MangaExtensionInstallService : Service() {
+
+ private var installer: InstallerManga? = null
+
+ override fun onCreate() {
+ val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
+ setSmallIcon(R.drawable.spinner_icon)
+ setAutoCancel(false)
+ setOngoing(true)
+ setShowWhen(false)
+ setContentTitle("Installing manga extension...")
+ setProgress(100, 100, true)
+ }.build()
+ startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val uri = intent?.data
+ val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
+ val installerUsed = intent?.getSerializableExtraCompat(
+ EXTRA_INSTALLER,
+ )
+ if (uri == null || id == null || installerUsed == null) {
+ stopSelf()
+ return START_NOT_STICKY
+ }
+
+ if (installer == null) {
+ installer = when (installerUsed) {
+ BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerManga(this)
+ else -> {
+ logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" }
+ stopSelf()
+ return START_NOT_STICKY
+ }
+ }
+ }
+ installer!!.addToQueue(id, uri)
+ return START_NOT_STICKY
+ }
+
+ override fun onDestroy() {
+ installer?.onDestroy()
+ installer = null
+ }
+
+ override fun onBind(i: Intent?): IBinder? = null
+
+ companion object {
+ private const val EXTRA_INSTALLER = "EXTRA_INSTALLER"
+
+ fun getIntent(
+ context: Context,
+ downloadId: Long,
+ uri: Uri,
+ installer: BasePreferences.ExtensionInstaller,
+ ): Intent {
+ return Intent(context, MangaExtensionInstallService::class.java)
+ .setDataAndType(uri, MangaExtensionInstaller.APK_MIME)
+ .putExtra(EXTRA_DOWNLOAD_ID, downloadId)
+ .putExtra(EXTRA_INSTALLER, installer)
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstaller.kt
new file mode 100644
index 00000000..88b2b422
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionInstaller.kt
@@ -0,0 +1,266 @@
+package eu.kanade.tachiyomi.extension.manga.util
+
+import android.app.DownloadManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
+import android.os.Environment
+import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
+import androidx.core.net.toUri
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.domain.base.BasePreferences
+import eu.kanade.tachiyomi.extension.InstallStep
+import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
+import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
+import eu.kanade.tachiyomi.util.storage.getUriCompat
+import logcat.LogPriority
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import tachiyomi.core.util.system.logcat
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+/**
+ * The installer which installs, updates and uninstalls the extensions.
+ *
+ * @param context The application context.
+ */
+internal class MangaExtensionInstaller(private val context: Context) {
+
+ /**
+ * The system's download manager
+ */
+ private val downloadManager = context.getSystemService()!!
+
+ /**
+ * 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()
+
+ /**
+ * Relay used to notify the installation step of every download.
+ */
+ private val downloadsRelay = PublishRelay.create>()
+
+ private val extensionInstaller = Injekt.get().extensionInstaller()
+
+ /**
+ * 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: MangaExtension) = Observable.defer {
+ val pkgName = extension.pkgName
+
+ val oldDownload = activeDownloads[pkgName]
+ if (oldDownload != null) {
+ deleteDownload(pkgName)
+ }
+
+ // 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 {
+ val query = DownloadManager.Query().setFilterById(id)
+
+ return Observable.interval(0, 1, TimeUnit.SECONDS)
+ // Get the current download status
+ .map {
+ downloadManager.query(query).use { cursor ->
+ cursor.moveToFirst()
+ cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
+ }
+ }
+ // 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)
+ else -> Observable.empty()
+ }
+ }
+ }
+
+ /**
+ * Starts an intent to install the extension at the given uri.
+ *
+ * @param uri The uri of the extension to install.
+ */
+ fun installApk(downloadId: Long, uri: Uri) {
+ when (val installer = extensionInstaller.get()) {
+ BasePreferences.ExtensionInstaller.LEGACY -> {
+ val intent = Intent(context, MangaExtensionInstallActivity::class.java)
+ .setDataAndType(uri, APK_MIME)
+ .putExtra(EXTRA_DOWNLOAD_ID, downloadId)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
+
+ context.startActivity(intent)
+ }
+ else -> {
+ val intent =
+ MangaExtensionInstallService.getIntent(context, downloadId, uri, installer)
+ ContextCompat.startForegroundService(context, intent)
+ }
+ }
+ }
+
+ /**
+ * Cancels extension install and remove from download manager and installer.
+ */
+ fun cancelInstall(pkgName: String) {
+ val downloadId = activeDownloads.remove(pkgName) ?: return
+ downloadManager.remove(downloadId)
+ InstallerManga.cancelInstallQueue(context, downloadId)
+ }
+
+ /**
+ * Starts an intent to uninstall the extension by the given package name.
+ *
+ * @param pkgName The package name of the extension to uninstall
+ */
+ fun uninstallApk(pkgName: String) {
+ val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ context.startActivity(intent)
+ }
+
+ /**
+ * 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)
+ context.registerReceiver(this, filter)
+ }
+
+ /**
+ * 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) {
+ logcat(LogPriority.ERROR) { "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)
+
+ installApk(id, File(localUri).getUriCompat(context))
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val APK_MIME = "application/vnd.android.package-archive"
+ const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
+ const val FILE_SCHEME = "file://"
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt
new file mode 100644
index 00000000..e5f1e5a0
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt
@@ -0,0 +1,232 @@
+package eu.kanade.tachiyomi.extension.manga.util
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.core.content.pm.PackageInfoCompat
+import dalvik.system.PathClassLoader
+import eu.kanade.domain.source.service.SourcePreferences
+import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
+import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.MangaSource
+import eu.kanade.tachiyomi.source.SourceFactory
+import eu.kanade.tachiyomi.util.lang.Hash
+import eu.kanade.tachiyomi.util.system.getApplicationIcon
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import logcat.LogPriority
+import tachiyomi.core.util.system.logcat
+import uy.kohesive.injekt.injectLazy
+
+/**
+ * Class that handles the loading of the extensions installed in the system.
+ */
+@SuppressLint("PackageManagerGetSignatures")
+internal object MangaExtensionLoader {
+
+ private val preferences: SourcePreferences by injectLazy()
+ private val loadNsfwSource by lazy {
+ preferences.showNsfwSource().get()
+ }
+
+ private const val EXTENSION_FEATURE = "tachiyomi.extension"
+ private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
+ private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
+ private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
+ private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
+ private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
+ const val LIB_VERSION_MIN = 1.2
+ const val LIB_VERSION_MAX = 1.5
+
+ private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
+
+ // inorichi's key
+ private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
+
+ /**
+ * List of the trusted signatures.
+ */
+ var trustedSignatures = mutableSetOf() + preferences.trustedSignatures().get() + officialSignature
+
+ /**
+ * Return a list of all the installed extensions initialized concurrently.
+ *
+ * @param context The application context.
+ */
+ fun loadMangaExtensions(context: Context): List {
+ val pkgManager = context.packageManager
+
+ @Suppress("DEPRECATION")
+ 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(it) }
+
+ if (extPkgs.isEmpty()) return emptyList()
+
+ // Load each extension concurrently and wait for completion
+ return runBlocking {
+ val deferred = extPkgs.map {
+ async { loadMangaExtension(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.
+ */
+ fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
+ val pkgInfo = try {
+ context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
+ } catch (error: PackageManager.NameNotFoundException) {
+ // Unlikely, but the package may have been uninstalled at this point
+ logcat(LogPriority.ERROR, error)
+ return MangaLoadResult.Error
+ }
+ if (!isPackageAnExtension(pkgInfo)) {
+ logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" }
+ return MangaLoadResult.Error
+ }
+ return loadMangaExtension(context, pkgName, pkgInfo)
+ }
+
+ /**
+ * Loads an extension given its package name.
+ *
+ * @param context The application context.
+ * @param pkgName The package name of the extension to load.
+ * @param pkgInfo The package info of the extension.
+ */
+ private fun loadMangaExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): MangaLoadResult {
+ 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
+ logcat(LogPriority.ERROR, error)
+ return MangaLoadResult.Error
+ }
+
+ val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
+ val versionName = pkgInfo.versionName
+ val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
+
+ if (versionName.isNullOrEmpty()) {
+ logcat(LogPriority.WARN) { "Missing versionName for extension $extName" }
+ return MangaLoadResult.Error
+ }
+
+ // Validate lib version
+ val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
+ if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
+ logcat(LogPriority.WARN) {
+ "Lib version is $libVersion, while only versions " +
+ "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
+ }
+ return MangaLoadResult.Error
+ }
+
+ val signatureHash = getSignatureHash(pkgInfo)
+
+ if (signatureHash == null) {
+ logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
+ return MangaLoadResult.Error
+ } else if (signatureHash !in trustedSignatures) {
+ val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
+ logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
+ return MangaLoadResult.Untrusted(extension)
+ }
+
+ val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
+ if (!loadNsfwSource && isNsfw) {
+ logcat(LogPriority.WARN) { "NSFW extension $pkgName not allowed" }
+ return MangaLoadResult.Error
+ }
+
+ val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
+ val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
+
+ val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
+
+ val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
+ .split(";")
+ .map {
+ val sourceClass = it.trim()
+ if (sourceClass.startsWith(".")) {
+ pkgInfo.packageName + sourceClass
+ } else {
+ sourceClass
+ }
+ }
+ .flatMap {
+ try {
+ when (val obj = Class.forName(it, false, classLoader).newInstance()) {
+ is MangaSource -> listOf(obj)
+ is SourceFactory -> obj.createSources()
+ else -> throw Exception("Unknown source class type! ${obj.javaClass}")
+ }
+ } catch (e: Throwable) {
+ logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
+ return MangaLoadResult.Error
+ }
+ }
+
+ val langs = sources.filterIsInstance()
+ .map { it.lang }
+ .toSet()
+ val lang = when (langs.size) {
+ 0 -> ""
+ 1 -> langs.first()
+ else -> "all"
+ }
+
+ val extension = MangaExtension.Installed(
+ name = extName,
+ pkgName = pkgName,
+ versionName = versionName,
+ versionCode = versionCode,
+ libVersion = libVersion,
+ lang = lang,
+ isNsfw = isNsfw,
+ hasReadme = hasReadme,
+ hasChangelog = hasChangelog,
+ sources = sources,
+ pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
+ isUnofficial = signatureHash != officialSignature,
+ icon = context.getApplicationIcon(pkgName),
+ )
+ return MangaLoadResult.Success(extension)
+ }
+
+ /**
+ * Returns true if the given package is an extension.
+ *
+ * @param pkgInfo The package info of the application.
+ */
+ private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
+ return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
+ }
+
+ /**
+ * Returns the signature hash of the package or null if it's not signed.
+ *
+ * @param pkgInfo The package info of the application.
+ */
+ private fun getSignatureHash(pkgInfo: PackageInfo): String? {
+ val signatures = pkgInfo.signatures
+ return if (signatures != null && signatures.isNotEmpty()) {
+ Hash.sha256(signatures.first().toByteArray())
+ } else {
+ null
+ }
+ }
+}
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/AndroidCookieJar.kt b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/AndroidCookieJar.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt
index bfaf034e..f9322e84 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/AndroidCookieJar.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.network
+package eu.kanade.tachiyomi.network
import android.webkit.CookieManager
import okhttp3.Cookie
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/DohProviders.kt b/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt
similarity index 99%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/DohProviders.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt
index e77c1871..95f448f1 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/DohProviders.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.network
+package eu.kanade.tachiyomi.network
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
similarity index 83%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/NetworkHelper.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
index 8368289c..8f5afb9f 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/NetworkHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
@@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.network
import android.content.Context
-import ani.dantotsu.aniyomi.util.network.AndroidCookieJar
-import ani.dantotsu.aniyomi.util.network.PREF_DOH_CLOUDFLARE
-import ani.dantotsu.aniyomi.util.network.PREF_DOH_GOOGLE
-import ani.dantotsu.aniyomi.util.network.dohCloudflare
-import ani.dantotsu.aniyomi.util.network.dohGoogle
-import ani.dantotsu.aniyomi.util.network.interceptor.CloudflareInterceptor
+import eu.kanade.tachiyomi.network.AndroidCookieJar
+import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
+import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
+import eu.kanade.tachiyomi.network.dohCloudflare
+import eu.kanade.tachiyomi.network.dohGoogle
+import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache
@@ -75,5 +75,5 @@ class NetworkHelper(
.build()
}
- fun defaultUserAgentProvider() = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0"//preferences.defaultUserAgent().get().trim()
+ fun defaultUserAgentProvider() = "Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"//preferences.defaultUserAgent().get().trim()
}
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/OkHttpExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/OkHttpExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt
index 5e241e03..aa523e87 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/OkHttpExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.network
-import ani.dantotsu.aniyomi.util.network.ProgressListener
-import ani.dantotsu.aniyomi.util.network.ProgressResponseBody
+import eu.kanade.tachiyomi.network.ProgressListener
+import eu.kanade.tachiyomi.network.ProgressResponseBody
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.DeserializationStrategy
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/ProgressListener.kt b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt
similarity index 70%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/ProgressListener.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt
index 9347fbad..2e219895 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/ProgressListener.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.network
+package eu.kanade.tachiyomi.network
interface ProgressListener {
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/ProgressResponseBody.kt b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt
similarity index 96%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/ProgressResponseBody.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt
index bd5f3b31..72248f17 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/ProgressResponseBody.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.network
+package eu.kanade.tachiyomi.network
import okhttp3.MediaType
import okhttp3.ResponseBody
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/Requests.kt b/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt
similarity index 95%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/Requests.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt
index 6adb0de8..43b1d467 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/Requests.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt
@@ -18,7 +18,9 @@ fun GET(
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
- return GET(url.toHttpUrl(), headers, cache)
+ val nUrl = url.toHttpUrl()
+ val g = GET(nUrl, headers, cache)
+ return g
}
/**
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
similarity index 94%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/CloudflareInterceptor.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
index 97b55b79..2d8202ea 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/CloudflareInterceptor.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt
@@ -1,14 +1,14 @@
-package ani.dantotsu.aniyomi.util.network.interceptor
+package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint
import android.content.Context
import android.webkit.WebView
import android.widget.Toast
import androidx.core.content.ContextCompat
-import ani.dantotsu.aniyomi.util.network.AndroidCookieJar
-import ani.dantotsu.aniyomi.util.system.WebViewClientCompat
-import ani.dantotsu.aniyomi.util.system.isOutdated
-import ani.dantotsu.aniyomi.util.toast
+import eu.kanade.tachiyomi.network.AndroidCookieJar
+import eu.kanade.tachiyomi.util.system.WebViewClientCompat
+import eu.kanade.tachiyomi.util.system.isOutdated
+import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt
new file mode 100644
index 00000000..6b101e35
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt
@@ -0,0 +1,105 @@
+package eu.kanade.tachiyomi.network.interceptor
+
+import android.os.SystemClock
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.Response
+import java.io.IOException
+import java.util.ArrayDeque
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+
+/**
+ * An OkHttp interceptor that handles rate limiting.
+ *
+ * Examples:
+ *
+ * permits = 5, period = 1, unit = seconds => 5 requests per second
+ * permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
+ *
+ * @since extension-lib 1.3
+ *
+ * @param permits {Int} Number of requests allowed within a period of units.
+ * @param period {Long} The limiting duration. Defaults to 1.
+ * @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
+ */
+fun OkHttpClient.Builder.rateLimit(
+ permits: Int,
+ period: Long = 1,
+ unit: TimeUnit = TimeUnit.SECONDS,
+) = addInterceptor(RateLimitInterceptor(null, permits, period, unit))
+
+/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+internal class RateLimitInterceptor(
+ private val host: String?,
+ private val permits: Int,
+ period: Long,
+ unit: TimeUnit,
+) : Interceptor {
+
+ private val requestQueue = ArrayDeque(permits)
+ private val rateLimitMillis = unit.toMillis(period)
+ private val fairLock = Semaphore(1, true)
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val call = chain.call()
+ if (call.isCanceled()) throw IOException("Canceled")
+
+ val request = chain.request()
+ when (host) {
+ null, request.url.host -> {} // need rate limit
+ else -> return chain.proceed(request)
+ }
+
+ try {
+ fairLock.acquire()
+ } catch (e: InterruptedException) {
+ throw IOException(e)
+ }
+
+ val requestQueue = this.requestQueue
+ val timestamp: Long
+
+ try {
+ synchronized(requestQueue) {
+ while (requestQueue.size >= permits) { // queue is full, remove expired entries
+ val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis
+ var hasRemovedExpired = false
+ while (requestQueue.isEmpty().not() && requestQueue.first <= periodStart) {
+ requestQueue.removeFirst()
+ hasRemovedExpired = true
+ }
+ if (call.isCanceled()) {
+ throw IOException("Canceled")
+ } else if (hasRemovedExpired) {
+ break
+ } else {
+ try { // wait for the first entry to expire, or notified by cached response
+ (requestQueue as Object).wait(requestQueue.first - periodStart)
+ } catch (_: InterruptedException) {
+ continue
+ }
+ }
+ }
+
+ // add request to queue
+ timestamp = SystemClock.elapsedRealtime()
+ requestQueue.addLast(timestamp)
+ }
+ } finally {
+ fairLock.release()
+ }
+
+ val response = chain.proceed(request)
+ if (response.networkResponse == null) { // response is cached, remove it from queue
+ synchronized(requestQueue) {
+ if (requestQueue.isEmpty() || timestamp < requestQueue.first) return@synchronized
+ requestQueue.removeFirstOccurrence(timestamp)
+ (requestQueue as Object).notifyAll()
+ }
+ }
+
+ return response
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt
new file mode 100644
index 00000000..5687c9f5
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt
@@ -0,0 +1,27 @@
+package eu.kanade.tachiyomi.network.interceptor
+
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import java.util.concurrent.TimeUnit
+
+/**
+ * An OkHttp interceptor that handles given url host's rate limiting.
+ *
+ * Examples:
+ *
+ * httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
+ * httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
+ *
+ * @since extension-lib 1.3
+ *
+ * @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
+ * @param permits {Int} Number of requests allowed within a period of units.
+ * @param period {Long} The limiting duration. Defaults to 1.
+ * @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
+ */
+fun OkHttpClient.Builder.rateLimitHost(
+ httpUrl: HttpUrl,
+ permits: Int,
+ period: Long = 1,
+ unit: TimeUnit = TimeUnit.SECONDS,
+) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period, unit))
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/UncaughtExceptionInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt
similarity index 100%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/UncaughtExceptionInterceptor.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/UserAgentInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt
similarity index 100%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/UserAgentInterceptor.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/WebViewInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt
similarity index 92%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/WebViewInterceptor.kt
rename to app/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt
index b322e962..5629f016 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/network/interceptor/WebViewInterceptor.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt
@@ -1,20 +1,20 @@
-package ani.dantotsu.aniyomi.util.network.interceptor
+package eu.kanade.tachiyomi.network.interceptor
import android.content.Context
import android.os.Build
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
-import ani.dantotsu.aniyomi.util.system.DeviceUtil
-import ani.dantotsu.aniyomi.util.system.WebViewUtil
-import ani.dantotsu.aniyomi.util.system.setDefaultSettings
-import ani.dantotsu.aniyomi.util.toast
+import eu.kanade.tachiyomi.util.system.DeviceUtil
+import eu.kanade.tachiyomi.util.system.WebViewUtil
+import eu.kanade.tachiyomi.util.system.setDefaultSettings
+import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.DelicateCoroutinesApi
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
-import ani.dantotsu.aniyomi.util.launchUI
+import tachiyomi.core.util.lang.launchUI
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/CatalogueSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt
similarity index 89%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/CatalogueSource.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt
index 559e94e6..de4137be 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/CatalogueSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt
@@ -1,7 +1,7 @@
-package ani.dantotsu.aniyomi.source
+package eu.kanade.tachiyomi.source
-import ani.dantotsu.aniyomi.source.model.FilterList
-import ani.dantotsu.aniyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
import rx.Observable
interface CatalogueSource : MangaSource {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt
new file mode 100644
index 00000000..430cc5bf
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt
@@ -0,0 +1,8 @@
+package eu.kanade.tachiyomi.source
+
+import eu.kanade.tachiyomi.PreferenceScreen
+
+interface ConfigurableSource : MangaSource {
+
+ fun setupPreferenceScreen(screen: PreferenceScreen)
+}
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/MangaSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/MangaSource.kt
similarity index 90%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/MangaSource.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/MangaSource.kt
index 9583b595..fabc4590 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/MangaSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/MangaSource.kt
@@ -1,9 +1,9 @@
-package ani.dantotsu.aniyomi.source
+package eu.kanade.tachiyomi.source
-import ani.dantotsu.aniyomi.source.model.Page
-import ani.dantotsu.aniyomi.source.model.SChapter
-import ani.dantotsu.aniyomi.source.model.SManga
-import ani.dantotsu.aniyomi.util.lang.awaitSingle
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.util.lang.awaitSingle
import rx.Observable
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceFactory.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceFactory.kt
new file mode 100644
index 00000000..f02df265
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceFactory.kt
@@ -0,0 +1,12 @@
+package eu.kanade.tachiyomi.source
+
+/**
+ * A factory for creating sources at runtime.
+ */
+interface SourceFactory {
+ /**
+ * Create a new copy of the sources
+ * @return The created sources
+ */
+ fun createSources(): List
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/UnmeteredSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/UnmeteredSource.kt
new file mode 100644
index 00000000..7536ccb0
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/UnmeteredSource.kt
@@ -0,0 +1,8 @@
+package eu.kanade.tachiyomi.source
+
+/**
+ * A source that explicitly doesn't require traffic considerations.
+ *
+ * This typically applies for self-hosted sources.
+ */
+interface UnmeteredSource
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/Filter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/Filter.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt
index 8e4d4fbf..f30b2f52 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/Filter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
sealed class Filter(val name: String, var state: T) {
open class Header(name: String) : Filter(name, 0)
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/FilterList.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt
similarity index 88%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/FilterList.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt
index f6ca750f..77f339b9 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/FilterList.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
data class FilterList(val list: List>) : List> by list {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/MangasPage.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt
similarity index 64%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/MangasPage.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt
index 27e8a901..a377c36e 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/MangasPage.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt
@@ -1,3 +1,3 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
data class MangasPage(val mangas: List, val hasNextPage: Boolean)
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/Page.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt
similarity index 84%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/Page.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt
index 018982e3..fca735f4 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/Page.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt
@@ -1,19 +1,17 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
import android.net.Uri
-import ani.dantotsu.aniyomi.util.network.ProgressListener
+import eu.kanade.tachiyomi.network.ProgressListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.Transient
+import java.io.Serializable
-@Serializable
open class Page(
val index: Int,
val url: String = "",
var imageUrl: String? = null,
@Transient var uri: Uri? = null, // Deprecated but can't be deleted due to extensions
-) : ProgressListener {
+) : Serializable, ProgressListener {
val number: Int
get() = index + 1
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/SChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt
similarity index 92%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/SChapter.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt
index e42a16c1..f53bbe8f 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/SChapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
import java.io.Serializable
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/SChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt
similarity index 85%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/SChapterImpl.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt
index db0253ff..4d5e43f1 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/SChapterImpl.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
class SChapterImpl : SChapter {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/SManga.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
index 3b296ad6..f0a014e2 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/SManga.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
import java.io.Serializable
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/SMangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt
similarity index 92%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/SMangaImpl.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt
index b181c904..91a7711c 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/SMangaImpl.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
class SMangaImpl : SManga {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/source/model/UpdateStrategy.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt
similarity index 93%
rename from app/src/main/java/ani/dantotsu/aniyomi/source/model/UpdateStrategy.kt
rename to app/src/main/java/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt
index 8f23b941..91b5f5e2 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/source/model/UpdateStrategy.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.source.model
+package eu.kanade.tachiyomi.source.model
/**
* Define the update strategy for a single [SManga].
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt
index f7c981c6..d0aa5b2d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt
@@ -5,12 +5,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
-import ani.dantotsu.aniyomi.source.CatalogueSource
-import ani.dantotsu.aniyomi.source.model.FilterList
-import ani.dantotsu.aniyomi.source.model.MangasPage
-import ani.dantotsu.aniyomi.source.model.Page
-import ani.dantotsu.aniyomi.source.model.SChapter
-import ani.dantotsu.aniyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt
new file mode 100644
index 00000000..76c68e88
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt
@@ -0,0 +1,25 @@
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.source.model.Page
+import rx.Observable
+
+fun HttpSource.fetchAllImageUrlsFromPageList(pages: List): Observable {
+ return Observable.from(pages)
+ .filter { !it.imageUrl.isNullOrEmpty() }
+ .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
+}
+
+private fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable {
+ return Observable.from(pages)
+ .filter { it.imageUrl.isNullOrEmpty() }
+ .concatMap { getImageUrl(it) }
+}
+
+private fun HttpSource.getImageUrl(page: Page): Observable {
+ page.status = Page.State.LOAD_PAGE
+ return fetchImageUrl(page)
+ .doOnError { page.status = Page.State.ERROR }
+ .onErrorReturn { null }
+ .doOnNext { page.imageUrl = it }
+ .map { page }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt
new file mode 100644
index 00000000..34376f84
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt
@@ -0,0 +1,201 @@
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+
+/**
+ * A simple implementation for sources from a website using Jsoup, an HTML parser.
+ */
+@Suppress("unused")
+abstract class ParsedHttpSource : HttpSource() {
+
+ /**
+ * Parses the response from the site and returns a [MangasPage] object.
+ *
+ * @param response the response from the site.
+ */
+ override fun popularMangaParse(response: Response): MangasPage {
+ val document = response.asJsoup()
+
+ val mangas = document.select(popularMangaSelector()).map { element ->
+ popularMangaFromElement(element)
+ }
+
+ val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
+ document.select(selector).first()
+ } != null
+
+ return MangasPage(mangas, hasNextPage)
+ }
+
+ /**
+ * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
+ */
+ protected abstract fun popularMangaSelector(): String
+
+ /**
+ * Returns a manga from the given [element]. Most sites only show the title and the url, it's
+ * totally fine to fill only those two values.
+ *
+ * @param element an element obtained from [popularMangaSelector].
+ */
+ protected abstract fun popularMangaFromElement(element: Element): SManga
+
+ /**
+ * Returns the Jsoup selector that returns the tag linking to the next page, or null if
+ * there's no next page.
+ */
+ protected abstract fun popularMangaNextPageSelector(): String?
+
+ /**
+ * Parses the response from the site and returns a [MangasPage] object.
+ *
+ * @param response the response from the site.
+ */
+ override fun searchMangaParse(response: Response): MangasPage {
+ val document = response.asJsoup()
+
+ val mangas = document.select(searchMangaSelector()).map { element ->
+ searchMangaFromElement(element)
+ }
+
+ val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
+ document.select(selector).first()
+ } != null
+
+ return MangasPage(mangas, hasNextPage)
+ }
+
+ /**
+ * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
+ */
+ protected abstract fun searchMangaSelector(): String
+
+ /**
+ * Returns a manga from the given [element]. Most sites only show the title and the url, it's
+ * totally fine to fill only those two values.
+ *
+ * @param element an element obtained from [searchMangaSelector].
+ */
+ protected abstract fun searchMangaFromElement(element: Element): SManga
+
+ /**
+ * Returns the Jsoup selector that returns the tag linking to the next page, or null if
+ * there's no next page.
+ */
+ protected abstract fun searchMangaNextPageSelector(): String?
+
+ /**
+ * Parses the response from the site and returns a [MangasPage] object.
+ *
+ * @param response the response from the site.
+ */
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ val document = response.asJsoup()
+
+ val mangas = document.select(latestUpdatesSelector()).map { element ->
+ latestUpdatesFromElement(element)
+ }
+
+ val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
+ document.select(selector).first()
+ } != null
+
+ return MangasPage(mangas, hasNextPage)
+ }
+
+ /**
+ * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
+ */
+ protected abstract fun latestUpdatesSelector(): String
+
+ /**
+ * Returns a manga from the given [element]. Most sites only show the title and the url, it's
+ * totally fine to fill only those two values.
+ *
+ * @param element an element obtained from [latestUpdatesSelector].
+ */
+ protected abstract fun latestUpdatesFromElement(element: Element): SManga
+
+ /**
+ * Returns the Jsoup selector that returns the tag linking to the next page, or null if
+ * there's no next page.
+ */
+ protected abstract fun latestUpdatesNextPageSelector(): String?
+
+ /**
+ * Parses the response from the site and returns the details of a manga.
+ *
+ * @param response the response from the site.
+ */
+ override fun mangaDetailsParse(response: Response): SManga {
+ return mangaDetailsParse(response.asJsoup())
+ }
+
+ /**
+ * Returns the details of the manga from the given [document].
+ *
+ * @param document the parsed document.
+ */
+ protected abstract fun mangaDetailsParse(document: Document): SManga
+
+ /**
+ * Parses the response from the site and returns a list of chapters.
+ *
+ * @param response the response from the site.
+ */
+ override fun chapterListParse(response: Response): List {
+ val document = response.asJsoup()
+ return document.select(chapterListSelector()).map { chapterFromElement(it) }
+ }
+
+ /**
+ * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
+ */
+ protected abstract fun chapterListSelector(): String
+
+ /**
+ * Returns a chapter from the given element.
+ *
+ * @param element an element obtained from [chapterListSelector].
+ */
+ protected abstract fun chapterFromElement(element: Element): SChapter
+
+ /**
+ * Parses the response from the site and returns the page list.
+ *
+ * @param response the response from the site.
+ */
+ override fun pageListParse(response: Response): List {
+ return pageListParse(response.asJsoup())
+ }
+
+ /**
+ * Returns a page list from the given document.
+ *
+ * @param document the parsed document.
+ */
+ protected abstract fun pageListParse(document: Document): List
+
+ /**
+ * Parse the response from the site and returns the absolute url to the source image.
+ *
+ * @param response the response from the site.
+ */
+ override fun imageUrlParse(response: Response): String {
+ return imageUrlParse(response.asJsoup())
+ }
+
+ /**
+ * Returns the absolute url to the source image from the document.
+ *
+ * @param document the parsed document.
+ */
+ protected abstract fun imageUrlParse(document: Document): String
+}
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/JsoupExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt
similarity index 100%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/JsoupExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/RxExtension.kt b/app/src/main/java/eu/kanade/tachiyomi/util/RxExtension.kt
similarity index 62%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/RxExtension.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/RxExtension.kt
index a3b23909..c0fc1ec3 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/RxExtension.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/RxExtension.kt
@@ -1,3 +1,3 @@
-package ani.dantotsu.aniyomi.util
+package eu.kanade.tachiyomi.util
//expect suspend fun Observable.awaitSingle(): T
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/lang/CloseableExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/CloseableExtensions.kt
similarity index 95%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/lang/CloseableExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/lang/CloseableExtensions.kt
index a2c6e072..647eaab3 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/lang/CloseableExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/CloseableExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.lang
+package eu.kanade.tachiyomi.util.lang
import java.io.Closeable
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/lang/Hash.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/Hash.kt
similarity index 96%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/lang/Hash.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/lang/Hash.kt
index ed087283..32d2a2d2 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/lang/Hash.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/Hash.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.lang
+package eu.kanade.tachiyomi.util.lang
import java.security.MessageDigest
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/lang/RxCoroutineBridge.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/RxCoroutineBridge.kt
similarity index 98%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/lang/RxCoroutineBridge.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/lang/RxCoroutineBridge.kt
index 608d57a1..57f9e9a0 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/lang/RxCoroutineBridge.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/RxCoroutineBridge.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.lang
+package eu.kanade.tachiyomi.util.lang
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.InternalCoroutinesApi
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/lang/StringExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt
similarity index 98%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/lang/StringExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt
index b100a856..97bb9168 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/lang/StringExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.lang
+package eu.kanade.tachiyomi.util.lang
import androidx.core.text.parseAsHtml
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt
new file mode 100644
index 00000000..c6076b79
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt
@@ -0,0 +1,31 @@
+package eu.kanade.tachiyomi.util.preference
+
+import android.widget.CompoundButton
+import eu.kanade.core.preference.PreferenceMutableState
+import kotlinx.coroutines.CoroutineScope
+import tachiyomi.core.preference.Preference
+
+/**
+ * Binds a checkbox or switch view with a boolean preference.
+ */
+fun CompoundButton.bindToPreference(pref: Preference) {
+ isChecked = pref.get()
+ setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
+}
+
+operator fun Preference>.plusAssign(item: T) {
+ set(get() + item)
+}
+
+operator fun Preference>.minusAssign(item: T) {
+ set(get() - item)
+}
+
+fun Preference.toggle(): Boolean {
+ set(!get())
+ return get()
+}
+
+fun Preference.asState(presenterScope: CoroutineScope): PreferenceMutableState {
+ return PreferenceMutableState(this, presenterScope)
+}
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/storage/FileExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt
similarity index 93%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/storage/FileExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt
index 5063fc07..4fd00702 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/storage/FileExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.storage
+package eu.kanade.tachiyomi.util.storage
import android.content.Context
import android.net.Uri
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/system/ContextExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
index c426a38f..773c8c12 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/system/ContextExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.system
+package eu.kanade.tachiyomi.util.system
import android.app.ActivityManager
import android.content.ClipData
@@ -21,9 +21,9 @@ import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.net.toUri
-import ani.dantotsu.aniyomi.util.lang.truncateCenter
+import eu.kanade.tachiyomi.util.lang.truncateCenter
import logcat.LogPriority
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.util.system.logcat
import ani.dantotsu.toast
import com.hippo.unifile.UniFile
import java.io.File
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/system/DeviceUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DeviceUtil.kt
similarity index 96%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/system/DeviceUtil.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/system/DeviceUtil.kt
index 244e655b..57950eda 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/system/DeviceUtil.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DeviceUtil.kt
@@ -1,9 +1,9 @@
-package ani.dantotsu.aniyomi.util.system
+package eu.kanade.tachiyomi.util.system
import android.annotation.SuppressLint
import android.os.Build
import logcat.LogPriority
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.util.system.logcat
object DeviceUtil {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/system/IntentExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/IntentExtensions.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/system/IntentExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/system/IntentExtensions.kt
index d6d6d0c6..5adba5d9 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/system/IntentExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/IntentExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.system
+package eu.kanade.tachiyomi.util.system
import android.content.ClipData
import android.content.Context
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/system/LocaleHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt
similarity index 97%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/system/LocaleHelper.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt
index 78958c16..cf0df960 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/system/LocaleHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.system
+package eu.kanade.tachiyomi.util.system
import android.content.Context
import androidx.core.os.LocaleListCompat
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/system/NotificationExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt
similarity index 98%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/system/NotificationExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt
index 9d85e88f..d43d7045 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/system/NotificationExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.system
+package eu.kanade.tachiyomi.util.system
import android.Manifest
import android.app.Notification
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/ToastExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ToastExtensions.kt
similarity index 95%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/ToastExtensions.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/system/ToastExtensions.kt
index 69f58530..4901e746 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/ToastExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ToastExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util
+package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.widget.Toast
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/system/WebViewClientCompat.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt
similarity index 98%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/system/WebViewClientCompat.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt
index 8e081e07..6c6fba4f 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/system/WebViewClientCompat.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.system
+package eu.kanade.tachiyomi.util.system
import android.annotation.TargetApi
import android.os.Build
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/system/WebViewUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt
similarity index 96%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/system/WebViewUtil.kt
rename to app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt
index defa5b9e..beef906d 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/system/WebViewUtil.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util.system
+package eu.kanade.tachiyomi.util.system
import android.annotation.SuppressLint
import android.content.Context
@@ -7,7 +7,7 @@ import android.webkit.CookieManager
import android.webkit.WebSettings
import android.webkit.WebView
import logcat.LogPriority
-import ani.dantotsu.aniyomi.util.logcat
+import tachiyomi.core.util.system.logcat
object WebViewUtil {
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/core/preference/Preference.kt b/app/src/main/java/tachiyomi/core/preference/Preference.kt
similarity index 91%
rename from app/src/main/java/ani/dantotsu/aniyomi/core/preference/Preference.kt
rename to app/src/main/java/tachiyomi/core/preference/Preference.kt
index 2b189e5f..c276141e 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/core/preference/Preference.kt
+++ b/app/src/main/java/tachiyomi/core/preference/Preference.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.core.preference
+package tachiyomi.core.preference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/core/preference/PreferenceStore.kt b/app/src/main/java/tachiyomi/core/preference/PreferenceStore.kt
similarity index 96%
rename from app/src/main/java/ani/dantotsu/aniyomi/core/preference/PreferenceStore.kt
rename to app/src/main/java/tachiyomi/core/preference/PreferenceStore.kt
index 76da52a3..f8cc9f89 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/core/preference/PreferenceStore.kt
+++ b/app/src/main/java/tachiyomi/core/preference/PreferenceStore.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.core.preference
+package tachiyomi.core.preference
interface PreferenceStore {
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/CoroutinesExtensions.kt b/app/src/main/java/tachiyomi/core/util/lang/CoroutinesExtensions.kt
similarity index 98%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/CoroutinesExtensions.kt
rename to app/src/main/java/tachiyomi/core/util/lang/CoroutinesExtensions.kt
index 1bcb7f55..4573b2d8 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/CoroutinesExtensions.kt
+++ b/app/src/main/java/tachiyomi/core/util/lang/CoroutinesExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util
+package tachiyomi.core.util.lang
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/util/LogcatExtensions.kt b/app/src/main/java/tachiyomi/core/util/system/LogcatExtensions.kt
similarity index 91%
rename from app/src/main/java/ani/dantotsu/aniyomi/util/LogcatExtensions.kt
rename to app/src/main/java/tachiyomi/core/util/system/LogcatExtensions.kt
index 4758a8ce..fb587b07 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/util/LogcatExtensions.kt
+++ b/app/src/main/java/tachiyomi/core/util/system/LogcatExtensions.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.util
+package tachiyomi.core.util.system
import logcat.LogPriority
import logcat.asLog
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/domain/category/model/Category.kt b/app/src/main/java/tachiyomi/domain/category/model/Category.kt
similarity index 85%
rename from app/src/main/java/ani/dantotsu/aniyomi/domain/category/model/Category.kt
rename to app/src/main/java/tachiyomi/domain/category/model/Category.kt
index 89663c99..46e2af94 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/domain/category/model/Category.kt
+++ b/app/src/main/java/tachiyomi/domain/category/model/Category.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.domain.category.model
+package tachiyomi.domain.category.model
import java.io.Serializable
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/domain/library/model/Flag.kt b/app/src/main/java/tachiyomi/domain/library/model/Flag.kt
similarity index 93%
rename from app/src/main/java/ani/dantotsu/aniyomi/domain/library/model/Flag.kt
rename to app/src/main/java/tachiyomi/domain/library/model/Flag.kt
index 659cad19..655d19bf 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/domain/library/model/Flag.kt
+++ b/app/src/main/java/tachiyomi/domain/library/model/Flag.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.domain.library.model
+package tachiyomi.domain.library.model
interface Flag {
val flag: Long
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/domain/library/model/LibraryDisplayMode.kt b/app/src/main/java/tachiyomi/domain/library/model/LibraryDisplayMode.kt
similarity index 93%
rename from app/src/main/java/ani/dantotsu/aniyomi/domain/library/model/LibraryDisplayMode.kt
rename to app/src/main/java/tachiyomi/domain/library/model/LibraryDisplayMode.kt
index 5db7ee3d..f21eb190 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/domain/library/model/LibraryDisplayMode.kt
+++ b/app/src/main/java/tachiyomi/domain/library/model/LibraryDisplayMode.kt
@@ -1,6 +1,6 @@
-package ani.dantotsu.aniyomi.domain.library.model
+package tachiyomi.domain.library.model
-import ani.dantotsu.aniyomi.domain.category.model.Category
+import tachiyomi.domain.category.model.Category
sealed class LibraryDisplayMode(
override val flag: Long,
diff --git a/app/src/main/java/ani/dantotsu/aniyomi/domain/source/anime/model/AnimeSourceData.kt b/app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSourceData.kt
similarity index 74%
rename from app/src/main/java/ani/dantotsu/aniyomi/domain/source/anime/model/AnimeSourceData.kt
rename to app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSourceData.kt
index 28895b5d..22bf2fce 100644
--- a/app/src/main/java/ani/dantotsu/aniyomi/domain/source/anime/model/AnimeSourceData.kt
+++ b/app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSourceData.kt
@@ -1,4 +1,4 @@
-package ani.dantotsu.aniyomi.domain.source.anime.model
+package tachiyomi.domain.source.anime.model
data class AnimeSourceData(
val id: Long,
diff --git a/app/src/main/java/tachiyomi/domain/source/manga/model/MangaSourceData.kt b/app/src/main/java/tachiyomi/domain/source/manga/model/MangaSourceData.kt
new file mode 100644
index 00000000..9b8571dc
--- /dev/null
+++ b/app/src/main/java/tachiyomi/domain/source/manga/model/MangaSourceData.kt
@@ -0,0 +1,10 @@
+package tachiyomi.domain.source.manga.model
+
+data class MangaSourceData(
+ val id: Long,
+ val lang: String,
+ val name: String,
+) {
+
+ val isMissingInfo: Boolean = name.isBlank() || lang.isBlank()
+}