lots of background work for manga extensions
This commit is contained in:
parent
dbe573131e
commit
57a584a820
123 changed files with 2676 additions and 553 deletions
|
@ -9,8 +9,7 @@
|
|||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"
|
||||
tools:ignore="ScopedStorage" />
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
NOTICE
|
||||
|
||||
This software includes code modified from Aniyomi, available at https://github.com/aniyomiorg/aniyomi/.
|
|
@ -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<Application>.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" }
|
||||
}
|
||||
}
|
||||
}*/
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package ani.dantotsu.aniyomi.util.srcapi
|
||||
|
||||
//actual suspend fun <T> Observable<T>.awaitSingle(): T = awaitSingle()
|
|
@ -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
|
||||
|
|
|
@ -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<MangaImage>()
|
||||
fun images(): List<MangaImage> = images
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Lazier<BaseParser>> = 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<Lazier<BaseParser>> = emptyList()
|
||||
|
||||
|
@ -52,13 +29,8 @@ object AnimeSources : WatchSources() {
|
|||
}
|
||||
|
||||
|
||||
|
||||
object HAnimeSources : WatchSources() {
|
||||
private val aList: List<Lazier<BaseParser>> = lazyList(
|
||||
//"HentaiMama" to ::HentaiMama,
|
||||
//"Haho" to ::Haho,
|
||||
//"HentaiStream" to ::HentaiStream,
|
||||
//"HentaiFF" to ::HentaiFF,
|
||||
)
|
||||
|
||||
override val list = listOf(aList,AnimeSources.list).flatten()
|
||||
|
|
|
@ -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<ShowResponse> {
|
||||
private fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List<ShowResponse> {
|
||||
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<String, String>?, sManga: SManga): List<MangaChapter> {
|
||||
val source = extension.sources.first()
|
||||
if (source is CatalogueSource) {
|
||||
try {
|
||||
val res = source.getChapterList(sManga)
|
||||
var chapterList: List<MangaChapter> = 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<MangaImage> {
|
||||
val source = extension.sources.first()
|
||||
if (source is HttpSource) {
|
||||
//try {
|
||||
val res = source.getPageList(sChapter)
|
||||
var chapterList: List<MangaImage> = 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<ShowResponse> {
|
||||
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<ShowResponse> {
|
||||
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<String>() // Populate as needed
|
||||
val total = 20
|
||||
val extra: Map<String, String>? = 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<String, String> = 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
|
||||
|
|
|
@ -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<String,String>?=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<String> = listOf(), total: Int? = null, extra: Map<String, String>?=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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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<String, MangaChapter> {
|
||||
val map = mutableMapOf<String, MangaChapter>()
|
||||
val parser = get(i)
|
||||
show.sManga?.let { sManga ->
|
||||
tryWithSuspend(true) {
|
||||
parser.loadChapters(show.link, show.extra).forEach {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, String>?): List<MangaChapter>
|
||||
abstract suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter>
|
||||
|
||||
/**
|
||||
* 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<String, String>?, latest: Float): MangaChapter? {
|
||||
return loadChapters(mangaLink, extra)
|
||||
open suspend fun getLatestChapter(mangaLink: String, extra: Map<String, String>?, 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<MangaImage>
|
||||
abstract suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage>
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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<Lazier<BaseParser>> = lazyList(
|
||||
)
|
||||
override var list: List<Lazier<BaseParser>> = emptyList()
|
||||
|
||||
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
|
||||
// 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<MangaExtension.Installed>): List<Lazier<BaseParser>> {
|
||||
return extensions.map { extension ->
|
||||
val name = extension.name
|
||||
Lazier({ DynamicMangaParser(extension) }, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object HMangaSources : MangaReadSources() {
|
||||
|
|
|
@ -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<ExtensionsAdapter.ViewHolder>() {
|
||||
|
||||
private var extensions: List<AnimeExtension.Installed> = emptyList()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T>(
|
||||
private val preference: Preference<T>,
|
||||
scope: CoroutineScope,
|
||||
) : MutableState<T> {
|
||||
|
||||
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 <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)
|
|
@ -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,
|
|
@ -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,
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.domain.source.service
|
||||
package eu.kanade.domain.source.service
|
||||
|
||||
class SetMigrateSorting(
|
||||
private val preferences: SourcePreferences,
|
|
@ -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,
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
package ani.dantotsu.aniyomi
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
typealias PreferenceScreen = androidx.preference.PreferenceScreen
|
|
@ -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
|
|
@ -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
|
||||
|
||||
/**
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.animesource
|
||||
package eu.kanade.tachiyomi.animesource
|
||||
|
||||
/**
|
||||
* A factory for creating sources at runtime.
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 {
|
|
@ -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 {
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.core
|
||||
package eu.kanade.tachiyomi.core
|
||||
|
||||
object Constants {
|
||||
const val URL_HELP = "https://aniyomi.org/help/"
|
|
@ -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<T>(
|
||||
private val preferences: SharedPreferences,
|
|
@ -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,
|
|
@ -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.
|
|
@ -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.
|
|
@ -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) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.util.extension
|
||||
package eu.kanade.tachiyomi.extension
|
||||
|
||||
enum class InstallStep {
|
||||
Idle, Pending, Downloading, Installing, Installed, Error;
|
|
@ -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<AnimeExtension.Installed>())
|
||||
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<AnimeLoadResult.Success>()
|
||||
|
@ -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<AnimeExtension.Available>) {
|
||||
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 })
|
||||
}
|
||||
}
|
|
@ -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<Long> by lazy {
|
||||
// preferenceStore.getLong("last_ext_check", 0)
|
||||
//}
|
||||
private val lastExtCheck: Long = 0
|
||||
private val lastExtCheck: Preference<Long> 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<AnimeExtension.Installed>? {
|
||||
// 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)
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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() {
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
||||
/**
|
|
@ -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<String, Drawable>()
|
||||
|
||||
private val _installedExtensionsFlow = MutableStateFlow(emptyList<MangaExtension.Installed>())
|
||||
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<MangaExtension.Available>())
|
||||
val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow()
|
||||
|
||||
private var availableExtensionsSourcesData: Map<Long, MangaSourceData> = emptyMap()
|
||||
|
||||
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<MangaExtension.Available>) {
|
||||
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<MangaExtension.Untrusted>())
|
||||
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<MangaLoadResult.Success>()
|
||||
.map { it.extension }
|
||||
|
||||
_untrustedExtensionsFlow.value = extensions
|
||||
.filterIsInstance<MangaLoadResult.Untrusted>()
|
||||
.map { it.extension }
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the available extensions in the [api] and updates [availableExtensions].
|
||||
*/
|
||||
suspend fun findAvailableExtensions() {
|
||||
val extensions: List<MangaExtension.Available> = 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<MangaExtension.Available>) {
|
||||
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<MangaExtension.Available>) {
|
||||
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<InstallStep> {
|
||||
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<InstallStep> {
|
||||
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 })
|
||||
}
|
||||
}
|
|
@ -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<Long> by lazy {
|
||||
preferenceStore.getLong("last_ext_check", 0)
|
||||
}
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<MangaExtension.Available> {
|
||||
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<List<ExtensionJsonObject>>()
|
||||
.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<MangaExtension.Installed>? {
|
||||
// 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<MangaLoadResult.Success>()
|
||||
.map { it.extension }
|
||||
|
||||
val extensionsWithUpdate = mutableListOf<MangaExtension.Installed>()
|
||||
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<ExtensionJsonObject>.toExtensions(): List<MangaExtension.Available> {
|
||||
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<ExtensionSourceJsonObject>.toExtensionSources(): List<AvailableMangaSources> {
|
||||
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<ExtensionSourceJsonObject>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionSourceJsonObject(
|
||||
val id: Long,
|
||||
val lang: String,
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
)
|
|
@ -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<Entry>(null)
|
||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>(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<Entry, Int>? = 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"
|
|
@ -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<MangaSource>,
|
||||
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<AvailableMangaSources>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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<MangaExtensionManager>()
|
||||
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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<BasePreferences.ExtensionInstaller>(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<DownloadManager>()!!
|
||||
|
||||
/**
|
||||
* The broadcast receiver which listens to download completion events.
|
||||
*/
|
||||
private val downloadReceiver = DownloadCompletionReceiver()
|
||||
|
||||
/**
|
||||
* The currently requested downloads, with the package name (unique id) as key, and the id
|
||||
* returned by the download manager.
|
||||
*/
|
||||
private val activeDownloads = hashMapOf<String, Long>()
|
||||
|
||||
/**
|
||||
* Relay used to notify the installation step of every download.
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
||||
|
||||
private val extensionInstaller = Injekt.get<BasePreferences>().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<InstallStep> {
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
|
||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
||||
// Get the current download status
|
||||
.map {
|
||||
downloadManager.query(query).use { cursor ->
|
||||
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://"
|
||||
}
|
||||
}
|
|
@ -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<String>() + preferences.trustedSignatures().get() + officialSignature
|
||||
|
||||
/**
|
||||
* Return a list of all the installed extensions initialized concurrently.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
|
||||
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<CatalogueSource>()
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.util.network
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import okhttp3.Cookie
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.util.network
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
|
@ -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()
|
||||
}
|
|
@ -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
|
|
@ -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)
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.util.network
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
|
@ -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
|
|
@ -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<Long>(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
|
||||
}
|
||||
}
|
|
@ -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))
|
|
@ -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
|
|
@ -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 {
|
|
@ -0,0 +1,8 @@
|
|||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import eu.kanade.tachiyomi.PreferenceScreen
|
||||
|
||||
interface ConfigurableSource : MangaSource {
|
||||
|
||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||
}
|
|
@ -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
|
||||
|
||||
/**
|
|
@ -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<MangaSource>
|
||||
}
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
package ani.dantotsu.aniyomi.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
class SChapterImpl : SChapter {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
class SMangaImpl : SManga {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.aniyomi.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
/**
|
||||
* Define the update strategy for a single [SManga].
|
|
@ -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
|
||||
|
|
|
@ -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<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||
}
|
||||
|
||||
private fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { it.imageUrl.isNullOrEmpty() }
|
||||
.concatMap { getImageUrl(it) }
|
||||
}
|
||||
|
||||
private fun HttpSource.getImageUrl(page: Page): Observable<Page> {
|
||||
page.status = Page.State.LOAD_PAGE
|
||||
return fetchImageUrl(page)
|
||||
.doOnError { page.status = Page.State.ERROR }
|
||||
.onErrorReturn { null }
|
||||
.doOnNext { page.imageUrl = it }
|
||||
.map { page }
|
||||
}
|
|
@ -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 <a> 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 <a> 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 <a> 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<SChapter> {
|
||||
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<Page> {
|
||||
return pageListParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page list from the given document.
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
protected abstract fun pageListParse(document: Document): List<Page>
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
package ani.dantotsu.aniyomi.util
|
||||
package eu.kanade.tachiyomi.util
|
||||
|
||||
//expect suspend fun <T> Observable<T>.awaitSingle(): T
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue