mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-12 16:06:16 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc165fa6bc | ||
|
|
dc959796e6 | ||
|
|
0b9f2bb019 | ||
|
|
6ddbd4760c | ||
|
|
d1270c7c83 | ||
|
|
79618e1963 | ||
|
|
da81646297 | ||
|
|
41b90e3a39 | ||
|
|
57a584a820 | ||
|
|
dbe573131e |
@@ -26,7 +26,7 @@ Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-o
|
||||
| Type | Status |
|
||||
| ---------------- | ------- |
|
||||
| Anime Extensions | Working |
|
||||
| Manga Extensions | Not Working |
|
||||
| Manga Extensions | "Working" |
|
||||
| Light Novel Extensions | Not Working |
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ android {
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "0.0.2"
|
||||
versionName "0.1.2"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
@@ -97,6 +97,9 @@ dependencies {
|
||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
|
||||
// string matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
|
||||
// Aniyomi
|
||||
implementation 'io.reactivex:rxjava:1.3.8'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
@@ -9,9 +16,9 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -48,7 +55,7 @@
|
||||
android:theme="@style/Theme.Dantotsu"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup"
|
||||
>
|
||||
android:banner="@drawable/ic_banner_foreground">
|
||||
<activity
|
||||
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
@@ -207,10 +214,22 @@
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".subcriptions.AlarmReceiver"
|
||||
@@ -243,7 +262,10 @@
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name=".aniyomi.anime.util.AnimeExtensionInstallService"
|
||||
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ 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.android.material.color.DynamicColors
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import logcat.AndroidLogcatLogger
|
||||
@@ -33,6 +34,11 @@ class App : MultiDexApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
|
||||
if(useMaterialYou) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||
|
||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
|
||||
@@ -2,6 +2,7 @@ package ani.dantotsu
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -17,6 +18,8 @@ import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -24,7 +27,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 +40,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 +64,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 +72,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
|
||||
@@ -212,6 +225,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//ViewPager
|
||||
|
||||
@@ -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,12 +1,17 @@
|
||||
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 android.content.Context
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
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 eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
@@ -18,16 +23,23 @@ class AppModule(val app: Application) : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { NetworkHelper(app) }
|
||||
addSingletonFactory { NetworkHelper(app, get()) }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
|
||||
addSingletonFactory { MangaExtensionManager(app) }
|
||||
|
||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
addSingleton(sharedPreferences)
|
||||
|
||||
addSingletonFactory {
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
}
|
||||
|
||||
addSingletonFactory { MangaCache() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +49,13 @@ class PreferenceModule(val application: Application) : InjektModule {
|
||||
AndroidPreferenceStore(application)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
NetworkPreferences(
|
||||
preferenceStore = get(),
|
||||
verboseLogging = false,
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
SourcePreferences(get())
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package ani.dantotsu.aniyomi.util.srcapi
|
||||
|
||||
//actual suspend fun <T> Observable<T>.awaitSingle(): T = awaitSingle()
|
||||
@@ -1,21 +1,30 @@
|
||||
package ani.dantotsu.connections.discord
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application.getProcessName
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||
import ani.dantotsu.startMainActivity
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = getProcessName()
|
||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||
}
|
||||
setContentView(R.layout.activity_discord)
|
||||
|
||||
val webView = findViewById<WebView>(R.id.discordWebview)
|
||||
|
||||
webView.apply {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.databaseEnabled = true
|
||||
|
||||
@@ -113,6 +113,10 @@ data class Media(
|
||||
this.relation = mediaEdge.relationType?.toString()
|
||||
}
|
||||
|
||||
fun mainName() = nameMAL ?: name ?: nameRomaji
|
||||
fun mainName() = name ?: nameMAL ?: nameRomaji
|
||||
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
|
||||
}
|
||||
}
|
||||
|
||||
object MediaSingleton {
|
||||
var media: Media? = null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.FragmentManager
|
||||
@@ -28,10 +30,20 @@ import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.AniyomiAdapter
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MediaDetailsViewModel : ViewModel() {
|
||||
val scrolledToTop = MutableLiveData(true)
|
||||
@@ -40,16 +52,26 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
saveData("$id-select", data, activity)
|
||||
}
|
||||
|
||||
|
||||
fun loadSelected(media: Media): Selected {
|
||||
return loadData<Selected>("${media.id}-select") ?: Selected().let {
|
||||
it.source = if (media.isAdult) 0 else when (media.anime != null) {
|
||||
true -> loadData("settings_def_anime_source") ?: 0
|
||||
else -> loadData("settings_def_manga_source") ?: 0
|
||||
val sharedPreferences = Injekt.get<SharedPreferences>()
|
||||
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
|
||||
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
|
||||
true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
|
||||
else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
|
||||
}
|
||||
it.preferDub = loadData("settings_prefer_dub") ?: false
|
||||
saveSelected(media.id, it)
|
||||
it
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
fun loadSelectedStringLocation(sourceName: String): Int {
|
||||
//find the location of the source in the list
|
||||
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
||||
if (location == -1) {location = 0}
|
||||
return location
|
||||
}
|
||||
|
||||
var continueMedia: Boolean? = null
|
||||
@@ -167,7 +189,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
val server = selected.server ?: return false
|
||||
val link = ep.link ?: return false
|
||||
|
||||
ep.extractors = mutableListOf(watchSources?.get(selected.source)?.let {
|
||||
ep.extractors = mutableListOf(watchSources?.get(selected.sourceIndex)?.let {
|
||||
selected.sourceIndex = selected.sourceIndex
|
||||
if (!post && !it.allowsPreloading) null
|
||||
else ep.sEpisode?.let { it1 ->
|
||||
it.loadSingleVideoServer(server, link, ep.extra,
|
||||
@@ -238,7 +261,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.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
|
||||
)
|
||||
if (post) mangaChapter.postValue(chapter)
|
||||
true
|
||||
@@ -261,7 +284,7 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
suspend fun autoSearchNovels(media: Media) {
|
||||
val source = novelSources[media.selected?.source ?: 0]
|
||||
val source = novelSources[media.selected?.sourceIndex?:0]
|
||||
tryWithSuspend(post = true) {
|
||||
if (source != null) {
|
||||
novelResponses.postValue(source.sortedSearch(media))
|
||||
|
||||
@@ -7,7 +7,8 @@ data class Selected(
|
||||
var recyclerStyle: Int? = null,
|
||||
var recyclerReversed: Boolean = false,
|
||||
var chip: Int = 0,
|
||||
var source: Int = 0,
|
||||
//var source: String = "",
|
||||
var sourceIndex: Int = 0,
|
||||
var preferDub: Boolean = false,
|
||||
var server: String? = null,
|
||||
var video: Int = 0,
|
||||
|
||||
@@ -57,7 +57,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||
binding.searchRecyclerView.visibility = View.GONE
|
||||
binding.searchProgress.visibility = View.VISIBLE
|
||||
|
||||
i = media!!.selected!!.source
|
||||
i = media!!.selected!!.sourceIndex
|
||||
|
||||
val source = if (media!!.anime != null) {
|
||||
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
|
||||
|
||||
@@ -68,7 +68,7 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
|
||||
//Source Selection
|
||||
val source = media.selected!!.source.let { if (it >= watchSources.names.size) 0 else it }
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||
binding.animeSource.setText(watchSources.names[source])
|
||||
watchSources[source].apply {
|
||||
|
||||
@@ -130,7 +130,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
async { model.loadKitsuEpisodes(media) },
|
||||
async { model.loadFillerEpisodes(media) }
|
||||
)
|
||||
model.loadEpisodes(media, media.selected!!.source)
|
||||
model.loadEpisodes(media, media.selected!!.sourceIndex)
|
||||
}
|
||||
loaded = true
|
||||
} else {
|
||||
@@ -140,7 +140,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
}
|
||||
model.getEpisodes().observe(viewLifecycleOwner) { loadedEpisodes ->
|
||||
if (loadedEpisodes != null) {
|
||||
val episodes = loadedEpisodes[media.selected!!.source]
|
||||
val episodes = loadedEpisodes[media.selected!!.sourceIndex]
|
||||
if (episodes != null) {
|
||||
episodes.forEach { (i, episode) ->
|
||||
if (media.anime?.fillerEpisodes != null) {
|
||||
@@ -206,8 +206,8 @@ class AnimeWatchFragment : Fragment() {
|
||||
media.anime?.episodes = null
|
||||
reload()
|
||||
val selected = model.loadSelected(media)
|
||||
model.watchSources?.get(selected.source)?.showUserTextListener = null
|
||||
selected.source = i
|
||||
model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null
|
||||
selected.sourceIndex = i
|
||||
selected.server = null
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
@@ -216,11 +216,11 @@ class AnimeWatchFragment : Fragment() {
|
||||
|
||||
fun onDubClicked(checked: Boolean) {
|
||||
val selected = model.loadSelected(media)
|
||||
model.watchSources?.get(selected.source)?.selectDub = checked
|
||||
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
||||
selected.preferDub = checked
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.source) }
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
|
||||
}
|
||||
|
||||
fun loadEpisodes(i: Int) {
|
||||
|
||||
@@ -817,7 +817,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
|
||||
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
||||
serverInfo.text = model.watchSources!!.names.getOrNull(media.selected!!.source) ?: model.watchSources!!.names[0]
|
||||
serverInfo.text = model.watchSources!!.names.getOrNull(media.selected!!.sourceIndex) ?: model.watchSources!!.names[0]
|
||||
|
||||
model.epChanged.observe(this) {
|
||||
epChanging = !it
|
||||
@@ -1353,7 +1353,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
if (media.selected!!.server != null)
|
||||
model.loadEpisodeSingleVideo(ep, selected, false)
|
||||
else
|
||||
model.loadEpisodeVideos(ep, selected.source, false)
|
||||
model.loadEpisodeVideos(ep, selected.sourceIndex, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadEpisodeVideos(ep, media!!.selected!!.source)
|
||||
model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
|
||||
withContext(Dispatchers.Main){
|
||||
binding.selectorProgressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
109
app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt
Normal file
109
app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
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.util.LruCache
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
data class ImageData(
|
||||
val page: Page,
|
||||
val source: HttpSource
|
||||
){
|
||||
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetch the image
|
||||
val response = httpSource.getImage(page)
|
||||
println("Response: ${response.code}")
|
||||
println("Response: ${response.message}")
|
||||
|
||||
// Convert the Response to an InputStream
|
||||
val inputStream = response.body?.byteStream()
|
||||
|
||||
// Convert InputStream to Bitmap
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
|
||||
inputStream?.close()
|
||||
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
||||
|
||||
return@withContext bitmap
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions
|
||||
println("An error occurred: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga")
|
||||
}
|
||||
|
||||
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { os ->
|
||||
bitmap.compress(format, quality, os)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
val file = File(directory, filename)
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
bitmap.compress(format, quality, outputStream)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Handle exception here
|
||||
println("Exception while saving image: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCache() {
|
||||
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt()
|
||||
private val cache = LruCache<String, ImageData>(maxMemory)
|
||||
|
||||
@Synchronized
|
||||
fun put(key: String, imageDate: ImageData) {
|
||||
cache.put(key, imageDate)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun get(key: String): ImageData? = cache.get(key)
|
||||
|
||||
@Synchronized
|
||||
fun remove(key: String) {
|
||||
cache.remove(key)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
cache.evictAll()
|
||||
}
|
||||
|
||||
fun size(): Int = cache.size()
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,8 @@ import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class MangaChapterAdapter(
|
||||
private var type: Int,
|
||||
@@ -63,12 +65,12 @@ class MangaChapterAdapter(
|
||||
val ep = arr[position]
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
if (media.userProgress != null) {
|
||||
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat())
|
||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||
else {
|
||||
binding.itemEpisodeViewedCover.visibility = View.GONE
|
||||
binding.itemEpisodeCont.setOnLongClickListener {
|
||||
updateProgress(media, ep.number)
|
||||
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString())
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -91,14 +93,14 @@ class MangaChapterAdapter(
|
||||
} else binding.itemChapterTitle.visibility = View.GONE
|
||||
|
||||
if (media.userProgress != null) {
|
||||
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) {
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat()) {
|
||||
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||
binding.itemEpisodeViewed.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.itemEpisodeViewedCover.visibility = View.GONE
|
||||
binding.itemEpisodeViewed.visibility = View.GONE
|
||||
binding.root.setOnLongClickListener {
|
||||
updateProgress(media, ep.number)
|
||||
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString())
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -113,4 +115,6 @@ class MangaChapterAdapter(
|
||||
fun updateType(t: Int) {
|
||||
type = t
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class MangaNameAdapter {
|
||||
companion object {
|
||||
fun findChapterNumber(text: String): Float? {
|
||||
val regex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)"
|
||||
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
|
||||
val matcher: Matcher = pattern.matcher(text)
|
||||
|
||||
return if (matcher.find()) {
|
||||
matcher.group(2)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class MangaReadAdapter(
|
||||
}
|
||||
|
||||
//Source Selection
|
||||
val source = media.selected!!.source.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||
binding.animeSource.setText(mangaReadSources.names[source])
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ open class MangaReadFragment : Fragment() {
|
||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.loadMangaChapters(media, media.selected!!.source)
|
||||
model.loadMangaChapters(media, media.selected!!.sourceIndex)
|
||||
}
|
||||
loaded = true
|
||||
} else {
|
||||
@@ -136,7 +136,7 @@ open class MangaReadFragment : Fragment() {
|
||||
|
||||
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters ->
|
||||
if (loadedChapters != null) {
|
||||
val chapters = loadedChapters[media.selected!!.source]
|
||||
val chapters = loadedChapters[media.selected!!.sourceIndex]
|
||||
if (chapters != null) {
|
||||
media.manga?.chapters = chapters
|
||||
|
||||
@@ -177,8 +177,8 @@ open class MangaReadFragment : Fragment() {
|
||||
media.manga?.chapters = null
|
||||
reload()
|
||||
val selected = model.loadSelected(media)
|
||||
model.mangaReadSources?.get(selected.source)?.showUserTextListener = null
|
||||
selected.source = i
|
||||
model.mangaReadSources?.get(selected.sourceIndex)?.showUserTextListener = null
|
||||
selected.sourceIndex = i
|
||||
selected.server = null
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
|
||||
@@ -14,15 +14,21 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
abstract class BaseImageAdapter(
|
||||
val activity: MangaReaderActivity,
|
||||
@@ -44,10 +50,33 @@ abstract class BaseImageAdapter(
|
||||
if (settings.layout != CurrentReaderSettings.Layouts.PAGED) {
|
||||
if (settings.padding) {
|
||||
when (settings.direction) {
|
||||
CurrentReaderSettings.Directions.TOP_TO_BOTTOM -> view.setPadding(0, 0, 0, 16f.px)
|
||||
CurrentReaderSettings.Directions.LEFT_TO_RIGHT -> view.setPadding(0, 0, 16f.px, 0)
|
||||
CurrentReaderSettings.Directions.BOTTOM_TO_TOP -> view.setPadding(0, 16f.px, 0, 0)
|
||||
CurrentReaderSettings.Directions.RIGHT_TO_LEFT -> view.setPadding(16f.px, 0, 0, 0)
|
||||
CurrentReaderSettings.Directions.TOP_TO_BOTTOM -> view.setPadding(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
16f.px
|
||||
)
|
||||
|
||||
CurrentReaderSettings.Directions.LEFT_TO_RIGHT -> view.setPadding(
|
||||
0,
|
||||
0,
|
||||
16f.px,
|
||||
0
|
||||
)
|
||||
|
||||
CurrentReaderSettings.Directions.BOTTOM_TO_TOP -> view.setPadding(
|
||||
0,
|
||||
16f.px,
|
||||
0,
|
||||
0
|
||||
)
|
||||
|
||||
CurrentReaderSettings.Directions.RIGHT_TO_LEFT -> view.setPadding(
|
||||
16f.px,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
view.updateLayoutParams {
|
||||
@@ -87,7 +116,7 @@ abstract class BaseImageAdapter(
|
||||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||
|
||||
companion object {
|
||||
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
||||
/*suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
||||
return tryWithSuspend {
|
||||
withContext(Dispatchers.IO) {
|
||||
Glide.with(this@loadBitmap)
|
||||
@@ -113,6 +142,43 @@ abstract class BaseImageAdapter(
|
||||
.get()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
||||
return tryWithSuspend {
|
||||
val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>()
|
||||
withContext(Dispatchers.IO) {
|
||||
Glide.with(this@loadBitmap)
|
||||
.asBitmap()
|
||||
.let {
|
||||
if (link.url.startsWith("file://")) {
|
||||
it.load(link.url)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
println("bitmap from cache")
|
||||
println(link.url)
|
||||
println(mangaCache.get(link.url))
|
||||
println("cache size: ${mangaCache.size()}")
|
||||
mangaCache.get(link.url)?.let { imageData ->
|
||||
val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap)
|
||||
it.load(bitmap)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
?.let {
|
||||
if (transforms.isNotEmpty()) {
|
||||
it.transform(*transforms.toTypedArray())
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
?.submit()
|
||||
?.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mergeBitmap(bitmap1: Bitmap, bitmap2: Bitmap, scale: Boolean = false): Bitmap {
|
||||
@@ -133,4 +199,8 @@ abstract class BaseImageAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface ImageFetcher {
|
||||
suspend fun fetchImage(page: Page): Bitmap?
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaSingleton
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.tryWith
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -49,7 +50,8 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
||||
activity?.runOnUiThread {
|
||||
tryWith { dismiss() }
|
||||
if(launch) {
|
||||
val intent = Intent(activity, MangaReaderActivity::class.java).apply { putExtra("media", m) }
|
||||
MediaSingleton.media = m
|
||||
val intent = Intent(activity, MangaReaderActivity::class.java)//.apply { putExtra("media", m) }
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ActivityMangaReaderBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaSingleton
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
@@ -45,14 +48,23 @@ import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
class MangaReaderActivity : AppCompatActivity() {
|
||||
private val mangaCache = Injekt.get<MangaCache>()
|
||||
|
||||
private lateinit var binding: ActivityMangaReaderBinding
|
||||
private val model: MediaDetailsViewModel by viewModels()
|
||||
private val scope = lifecycleScope
|
||||
@@ -106,6 +118,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mangaCache.clear()
|
||||
rpc?.close()
|
||||
super.onDestroy()
|
||||
}
|
||||
@@ -158,10 +171,13 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
media = if (model.getMedia().value == null)
|
||||
try {
|
||||
(intent.getSerialized("media")) ?: return
|
||||
//(intent.getSerialized("media")) ?: return
|
||||
MediaSingleton.media ?: return
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
return
|
||||
} finally {
|
||||
MediaSingleton.media = null
|
||||
}
|
||||
else model.getMedia().value ?: return
|
||||
model.setMedia(media)
|
||||
@@ -174,7 +190,30 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
|
||||
binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE
|
||||
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.source]
|
||||
if(model.mangaReadSources!!.names.isEmpty()){
|
||||
//try to reload sources
|
||||
try {
|
||||
if (media.isAdult) {
|
||||
val mangaSources = MangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
}else{
|
||||
val mangaSources = HMangaSources
|
||||
val scope = lifecycleScope
|
||||
scope.launch(Dispatchers.IO) {
|
||||
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
|
||||
}
|
||||
model.mangaReadSources = mangaSources
|
||||
}
|
||||
}catch (e: Exception){
|
||||
Firebase.crashlytics.recordException(e)
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex]
|
||||
|
||||
binding.mangaReaderTitle.text = media.userPreferredName
|
||||
|
||||
@@ -205,6 +244,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
//Chapter Change
|
||||
fun change(index: Int) {
|
||||
mangaCache.clear()
|
||||
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
|
||||
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog")
|
||||
}
|
||||
@@ -258,7 +298,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
type = RPC.Type.WATCHING
|
||||
activityName = media.userPreferredName
|
||||
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number)
|
||||
state = "Chapter : ${chap.number}/${media.manga?.totalChapters ?: "??"}"
|
||||
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}"
|
||||
media.cover?.let { cover ->
|
||||
largeImage = RPC.Link(media.userPreferredName, cover)
|
||||
}
|
||||
@@ -670,7 +710,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
progressDialog?.setCancelable(false)
|
||||
?.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
||||
saveData("${media.id}_save_progress", true)
|
||||
updateProgress(media, media.manga!!.selectedChapter!!)
|
||||
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString())
|
||||
dialog.dismiss()
|
||||
runnable.run()
|
||||
}
|
||||
@@ -682,7 +722,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
progressDialog?.show()
|
||||
} else {
|
||||
if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true)
|
||||
updateProgress(media, media.manga!!.selectedChapter!!)
|
||||
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString())
|
||||
runnable.run()
|
||||
}
|
||||
} else {
|
||||
@@ -691,7 +731,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun getTransformation(mangaImage: MangaImage): BitmapTransformation? {
|
||||
return model.loadTransformation(mangaImage, media.selected!!.source)
|
||||
return model.loadTransformation(mangaImage, media.selected!!.sourceIndex)
|
||||
}
|
||||
|
||||
fun onImageLongClicked(
|
||||
|
||||
@@ -35,7 +35,7 @@ class NovelReadAdapter(
|
||||
|
||||
fun search(): Boolean {
|
||||
val query = binding.searchBarText.text.toString()
|
||||
val source = media.selected!!.source.let { if (it >= novelReadSources.names.size) 0 else it }
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it }
|
||||
fragment.source = source
|
||||
|
||||
binding.searchBarText.clearFocus()
|
||||
@@ -44,7 +44,7 @@ class NovelReadAdapter(
|
||||
return true
|
||||
}
|
||||
|
||||
val source = media.selected!!.source.let { if (it >= novelReadSources.names.size) 0 else it }
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it }
|
||||
if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) {
|
||||
binding.animeSource.setText(novelReadSources.names[source], false)
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class NovelReadFragment : Fragment() {
|
||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter)
|
||||
loaded = true
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
search(searchQuery, sel?.source ?: 0, auto = sel?.server == null)
|
||||
search(searchQuery, sel?.sourceIndex ?: 0, auto = sel?.server == null)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ class NovelReadFragment : Fragment() {
|
||||
|
||||
fun onSourceChange(i: Int) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.source = i
|
||||
selected.sourceIndex = i
|
||||
source = i
|
||||
selected.server = null
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -171,6 +171,17 @@ abstract class AnimeParser : BaseParser() {
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyAnimeParser: AnimeParser() {
|
||||
override val name: String = "None"
|
||||
override val saveName: String = "None"
|
||||
|
||||
override val isDubAvailableSeparately: Boolean = false
|
||||
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> = emptyList()
|
||||
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> = emptyList()
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> = emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for containing Episode data of a particular parser
|
||||
* **/
|
||||
|
||||
@@ -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,17 +1,53 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
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 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 ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
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
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.*
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class AniyomiAdapter {
|
||||
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
|
||||
@@ -30,63 +66,64 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
override val saveName = extension.name
|
||||
override val hostUrl = extension.sources.first().name
|
||||
override val isDubAvailableSeparately = false
|
||||
override val isNSFW = extension.isNsfw
|
||||
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
|
||||
val source = extension.sources.first()
|
||||
if (source is AnimeCatalogueSource) {
|
||||
var res: SEpisode? = null
|
||||
try {
|
||||
val res = source.getEpisodeList(sAnime)
|
||||
var EpisodeList: List<Episode> = emptyList()
|
||||
for (episode in res) {
|
||||
println("episode: $episode")
|
||||
EpisodeList += SEpisodeToEpisode(episode)
|
||||
}
|
||||
return EpisodeList
|
||||
}
|
||||
catch (e: Exception) {
|
||||
|
||||
// Sort episodes by episode_number
|
||||
val sortedEpisodes = res.sortedBy { it.episode_number }
|
||||
|
||||
// Transform SEpisode objects to Episode objects
|
||||
|
||||
return sortedEpisodes.map { SEpisodeToEpisode(it) }
|
||||
} catch (e: Exception) {
|
||||
println("Exception: $e")
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
|
||||
}
|
||||
|
||||
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> {
|
||||
val source = extension.sources.first()
|
||||
if (source is AnimeCatalogueSource) {
|
||||
val video = source.getVideoList(sEpisode)
|
||||
var VideoList: List<VideoServer> = emptyList()
|
||||
for (videoServer in video) {
|
||||
VideoList += VideoToVideoServer(videoServer)
|
||||
}
|
||||
return VideoList
|
||||
val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
|
||||
|
||||
return try {
|
||||
val videos = source.getVideoList(sEpisode)
|
||||
videos.map { VideoToVideoServer(it) }
|
||||
} catch (e: Exception) {
|
||||
logger("Exception occurred: ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
|
||||
return VideoServerPassthrough(server)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val source = extension.sources.first()
|
||||
if (source is AnimeCatalogueSource) {
|
||||
val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
|
||||
|
||||
var res: AnimesPage? = null
|
||||
try {
|
||||
res = source.fetchSearchAnime(0, query, AnimeFilterList()).toBlocking().first()
|
||||
println("res: $res")
|
||||
return try {
|
||||
val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first()
|
||||
convertAnimesPageToShowResponse(res)
|
||||
} catch (e: CloudflareBypassException) {
|
||||
logger("Exception in search: $e")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
logger("Exception: $e")
|
||||
}
|
||||
|
||||
val conv = convertAnimesPageToShowResponse(res!!)
|
||||
return conv
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
logger("General exception in search: $e")
|
||||
emptyList()
|
||||
}
|
||||
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
|
||||
@@ -101,70 +138,328 @@ 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 mangaCache = Injekt.get<MangaCache>()
|
||||
val extension: MangaExtension.Installed
|
||||
init {
|
||||
this.extension = extension
|
||||
}
|
||||
override val server: VideoServer
|
||||
get() {
|
||||
return videoServer
|
||||
override val name = extension.name
|
||||
override val saveName = extension.name
|
||||
override val hostUrl = extension.sources.first().name
|
||||
override val isNSFW = extension.isNsfw
|
||||
|
||||
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
|
||||
val source = extension.sources.first() as? CatalogueSource ?: return emptyList()
|
||||
|
||||
return try {
|
||||
val res = source.getChapterList(sManga)
|
||||
val reversedRes = res.reversed()
|
||||
val chapterList = reversedRes.map { SChapterToMangaChapter(it) }
|
||||
logger("chapterList size: ${chapterList.size}")
|
||||
logger("chapterList: ${chapterList[1].title}")
|
||||
logger("chapterList: ${chapterList[1].description}")
|
||||
chapterList
|
||||
} catch (e: Exception) {
|
||||
logger("loadChapters Exception: $e")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||
val source = extension.sources.first() as? HttpSource ?: return emptyList()
|
||||
|
||||
return coroutineScope {
|
||||
try {
|
||||
println("source.name " + source.name)
|
||||
val res = source.getPageList(sChapter)
|
||||
val reIndexedPages = res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
|
||||
val deferreds = reIndexedPages.map { page ->
|
||||
async(Dispatchers.IO) {
|
||||
mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
|
||||
logger("put page: ${page.imageUrl}")
|
||||
pageToMangaImage(page)
|
||||
}
|
||||
}
|
||||
|
||||
deferreds.awaitAll()
|
||||
} catch (e: Exception) {
|
||||
logger("loadImages Exception: $e")
|
||||
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT).show()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetch the image
|
||||
val response = httpSource.getImage(page)
|
||||
println("Response: ${response.code}")
|
||||
println("Response: ${response.message}")
|
||||
|
||||
// Convert the Response to an InputStream
|
||||
val inputStream = response.body?.byteStream()
|
||||
|
||||
// Convert InputStream to Bitmap
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
|
||||
inputStream?.close()
|
||||
ani.dantotsu.media.manga.saveImage(
|
||||
bitmap,
|
||||
context.contentResolver,
|
||||
page.imageUrl!!,
|
||||
Bitmap.CompressFormat.JPEG,
|
||||
100
|
||||
)
|
||||
|
||||
return@withContext bitmap
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions
|
||||
println("An error occurred: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
// Fetch the image
|
||||
val response = httpSource.getImage(page)
|
||||
|
||||
// Convert the Response to an InputStream
|
||||
val inputStream = response.body?.byteStream()
|
||||
|
||||
// Convert InputStream to Bitmap
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
// Save the Bitmap using MediaStore API
|
||||
saveImage(bitmap, contentResolver, "image_${System.currentTimeMillis()}.jpg", Bitmap.CompressFormat.JPEG, 100)
|
||||
}
|
||||
|
||||
inputStream?.close()
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions
|
||||
println("An error occurred: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime")
|
||||
}
|
||||
|
||||
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { os ->
|
||||
bitmap.compress(format, quality, os)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
val file = File(directory, filename)
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
bitmap.compress(format, quality, outputStream)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Handle exception here
|
||||
println("Exception while saving image: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val source = extension.sources.first() as? HttpSource ?: return emptyList()
|
||||
|
||||
return try {
|
||||
val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
|
||||
logger("res observable: $res")
|
||||
convertMangasPageToShowResponse(res)
|
||||
} catch (e: CloudflareBypassException) {
|
||||
logger("Exception in search: $e")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
logger("General exception in search: $e")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 = 1
|
||||
val extra: Map<String, String>? = null // Populate as needed
|
||||
|
||||
// Create a new ShowResponse
|
||||
ShowResponse(name, link, coverUrl, sManga)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pageToMangaImage(page: Page): MangaImage {
|
||||
var headersMap = mapOf<String, String>()
|
||||
var urlWithoutHeaders = ""
|
||||
var url = ""
|
||||
|
||||
page.imageUrl?.let {
|
||||
val splitUrl = it.split("&")
|
||||
urlWithoutHeaders = splitUrl.getOrNull(0) ?: ""
|
||||
url = it
|
||||
|
||||
headersMap = splitUrl.mapNotNull { part ->
|
||||
val idx = part.indexOf("=")
|
||||
if (idx != -1) {
|
||||
try {
|
||||
val key = URLDecoder.decode(part.substring(0, idx), "UTF-8")
|
||||
val value = URLDecoder.decode(part.substring(idx + 1), "UTF-8")
|
||||
Pair(key, value)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
return MangaImage(
|
||||
FileUrl(url, headersMap),
|
||||
false,
|
||||
page
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
|
||||
/*val parsedChapterTitle = parseChapterTitle(sChapter.name)
|
||||
val number = if (sChapter.chapter_number.toInt() != -1){
|
||||
sChapter.chapter_number.toString()
|
||||
} else if(parsedChapterTitle.first != null || parsedChapterTitle.second != null){
|
||||
(parsedChapterTitle.first ?: "") + "." + (parsedChapterTitle.second ?: "")
|
||||
}else{
|
||||
sChapter.name
|
||||
}*/
|
||||
return MangaChapter(
|
||||
sChapter.name,
|
||||
sChapter.url,
|
||||
//if (parsedChapterTitle.first != null || parsedChapterTitle.second != null) {
|
||||
// parsedChapterTitle.third
|
||||
//} else {
|
||||
sChapter.name,
|
||||
//},
|
||||
null,
|
||||
sChapter
|
||||
)
|
||||
}
|
||||
|
||||
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
|
||||
val volumePattern = Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
||||
val chapterPattern = Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
||||
|
||||
val volumeMatcher = volumePattern.matcher(title)
|
||||
val chapterMatcher = chapterPattern.matcher(title)
|
||||
|
||||
val volumeNumber = if (volumeMatcher.find()) volumeMatcher.group(1) else null
|
||||
val chapterNumber = if (chapterMatcher.find()) chapterMatcher.group(1) else null
|
||||
|
||||
var remainingTitle = title
|
||||
if (volumeNumber != null) {
|
||||
remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
}
|
||||
if (chapterNumber != null) {
|
||||
remainingTitle = chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
}
|
||||
|
||||
return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||
override val server: VideoServer
|
||||
get() = videoServer
|
||||
|
||||
override suspend fun extract(): VideoContainer {
|
||||
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
|
||||
var subList: List<Subtitle> = emptyList()
|
||||
for(sub in videoServer.video?.subtitleTracks ?: emptyList()) {
|
||||
subList += TrackToSubtitle(sub)
|
||||
}
|
||||
if(vidList.isEmpty()) {
|
||||
val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList()
|
||||
|
||||
return if (vidList.isNotEmpty()) {
|
||||
VideoContainer(vidList, subList)
|
||||
} else {
|
||||
throw Exception("No videos found")
|
||||
}else{
|
||||
return VideoContainer(vidList, subList)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video {
|
||||
//try to find the number value from the .quality string
|
||||
val regex = Regex("""\d+""")
|
||||
val result = regex.find(aniVideo.quality)
|
||||
val number = result?.value?.toInt() ?: 0
|
||||
// Find the number value from the .quality string
|
||||
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
|
||||
|
||||
// Check for null video URL
|
||||
val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null")
|
||||
|
||||
val urlObj = URL(videoUrl)
|
||||
val path = urlObj.path
|
||||
val query = urlObj.query
|
||||
|
||||
var format = getVideoType(path)
|
||||
|
||||
var format = when {
|
||||
path.endsWith(".mp4", ignoreCase = true) || videoUrl.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
|
||||
path.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
|
||||
path.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
||||
else -> null
|
||||
}
|
||||
if (format == null) {
|
||||
if (format == null && query != null) {
|
||||
val queryPairs: List<Pair<String, String>> = query.split("&").map {
|
||||
val idx = it.indexOf("=")
|
||||
val key = URLDecoder.decode(it.substring(0, idx), "UTF-8")
|
||||
@@ -175,13 +470,9 @@ class VideoServerPassthrough : VideoExtractor{
|
||||
// Assume the file is named under the "file" query parameter
|
||||
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
|
||||
|
||||
format = when {
|
||||
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
|
||||
fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
|
||||
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
||||
else -> null
|
||||
}
|
||||
format = getVideoType(fileName)
|
||||
}
|
||||
|
||||
// If the format is still undetermined, log an error or handle it appropriately
|
||||
if (format == null) {
|
||||
logger("Unknown video format: $videoUrl")
|
||||
@@ -198,6 +489,15 @@ class VideoServerPassthrough : VideoExtractor{
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVideoType(fileName: String): VideoType? {
|
||||
return when {
|
||||
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
|
||||
fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
|
||||
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun TrackToSubtitle(track: Track, type: SubtitleType = SubtitleType.VTT): Subtitle {
|
||||
return Subtitle(track.lang, track.url, type)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ 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
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
|
||||
|
||||
abstract class BaseParser {
|
||||
|
||||
@@ -54,21 +57,41 @@ abstract class BaseParser {
|
||||
setUserText("Searching : ${mediaObj.mainName()}")
|
||||
val results = search(mediaObj.mainName())
|
||||
val sortedResults = if (results.isNotEmpty()) {
|
||||
StringMatcher.closestShowMovedToTop(mediaObj.mainName(), results)
|
||||
results.sortedByDescending { FuzzySearch.ratio(it.name, mediaObj.mainName()) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
response = sortedResults.firstOrNull()
|
||||
|
||||
if (response == null) {
|
||||
if (response == null || FuzzySearch.ratio(response.name, mediaObj.mainName()) < 100) {
|
||||
setUserText("Searching : ${mediaObj.nameRomaji}")
|
||||
val romajiResults = search(mediaObj.nameRomaji)
|
||||
val sortedRomajiResults = if (romajiResults.isNotEmpty()) {
|
||||
StringMatcher.closestShowMovedToTop(mediaObj.nameRomaji, romajiResults)
|
||||
romajiResults.sortedByDescending { FuzzySearch.ratio(it.name, mediaObj.nameRomaji) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
response = sortedRomajiResults.firstOrNull()
|
||||
val closestRomaji = sortedRomajiResults.firstOrNull()
|
||||
logger("Closest match from RomajiResults: ${closestRomaji?.name ?: "None"}")
|
||||
|
||||
response = if (response == null) {
|
||||
logger("No exact match found in results. Using closest match from RomajiResults.")
|
||||
closestRomaji
|
||||
} else {
|
||||
val romajiRatio = FuzzySearch.ratio(closestRomaji?.name ?: "", mediaObj.nameRomaji)
|
||||
val mainNameRatio = FuzzySearch.ratio(response.name, mediaObj.mainName())
|
||||
logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name}")
|
||||
logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name ?: "None"}")
|
||||
|
||||
if (romajiRatio > mainNameRatio) {
|
||||
logger("RomajiResults has a closer match. Replacing response.")
|
||||
closestRomaji
|
||||
} else {
|
||||
logger("Results has a closer or equal match. Keeping existing response.")
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
saveShowResponse(mediaObj.id, response)
|
||||
}
|
||||
@@ -141,7 +164,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 +183,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
|
||||
@@ -10,9 +11,11 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
abstract class WatchSources : BaseSources() {
|
||||
|
||||
override operator fun get(i: Int): AnimeParser {
|
||||
return (list.getOrNull(i)?:list[0]).get.value as AnimeParser
|
||||
return (list.getOrNull(i) ?: list.firstOrNull())?.get?.value as? AnimeParser
|
||||
?: EmptyAnimeParser()
|
||||
}
|
||||
|
||||
|
||||
suspend fun loadEpisodesFromMedia(i: Int, media: Media): MutableMap<String, Episode> {
|
||||
return tryWithSuspend(true) {
|
||||
val res = get(i).autoSearch(media) ?: return@tryWithSuspend mutableMapOf()
|
||||
@@ -39,7 +42,8 @@ abstract class WatchSources : BaseSources() {
|
||||
abstract class MangaReadSources : BaseSources() {
|
||||
|
||||
override operator fun get(i: Int): MangaParser {
|
||||
return (list.getOrNull(i)?:list[0]).get.value as MangaParser
|
||||
return (list.getOrNull(i)?:list.firstOrNull())?.get?.value as? MangaParser
|
||||
?: EmptyMangaParser()
|
||||
}
|
||||
|
||||
suspend fun loadChaptersFromMedia(i: Int, media: Media): MutableMap<String, MangaChapter> {
|
||||
@@ -52,11 +56,17 @@ abstract class MangaReadSources : BaseSources() {
|
||||
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
|
||||
val map = mutableMapOf<String, MangaChapter>()
|
||||
val parser = get(i)
|
||||
tryWithSuspend(true) {
|
||||
parser.loadChapters(show.link, show.extra).forEach {
|
||||
map[it.number] = MangaChapter(it)
|
||||
show.sManga?.let { sManga ->
|
||||
tryWithSuspend(true) {
|
||||
parser.loadChapters(show.link, show.extra, sManga).forEach {
|
||||
map[it.number] = MangaChapter(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
if(show.sManga == null) {
|
||||
logger("sManga is null")
|
||||
}
|
||||
logger("map size ${map.size}")
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
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 +14,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 +22,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,9 +31,9 @@ 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? {
|
||||
/*override suspend fun autoSearch(mediaObj: Media): ShowResponse? {
|
||||
var response = loadSavedShowResponse(mediaObj.id)
|
||||
if (response != null) {
|
||||
saveShowResponse(mediaObj.id, response, true)
|
||||
@@ -44,11 +48,22 @@ abstract class MangaParser : BaseParser() {
|
||||
saveShowResponse(mediaObj.id, response)
|
||||
}
|
||||
return response
|
||||
}
|
||||
}*/
|
||||
|
||||
open fun getTransformation(): BitmapTransformation? = null
|
||||
}
|
||||
|
||||
class EmptyMangaParser: MangaParser() {
|
||||
override val name: String = "None"
|
||||
override val saveName: String = "None"
|
||||
|
||||
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> = emptyList()
|
||||
|
||||
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> = emptyList()
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> = emptyList()
|
||||
}
|
||||
|
||||
data class MangaChapter(
|
||||
/**
|
||||
* Number of the Chapter in "String",
|
||||
@@ -65,6 +80,8 @@ data class MangaChapter(
|
||||
//Self-Descriptive
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
|
||||
val sChapter: SChapter,
|
||||
)
|
||||
|
||||
data class MangaImage(
|
||||
@@ -75,8 +92,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,14 +2,36 @@ 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() {
|
||||
val aList: List<Lazier<BaseParser>> = lazyList(
|
||||
)
|
||||
val aList: List<Lazier<BaseParser>> = lazyList()
|
||||
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
|
||||
//todo
|
||||
}
|
||||
override val list = listOf(aList,MangaSources.list).flatten()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.logger
|
||||
|
||||
class StringMatcher {
|
||||
companion object {
|
||||
private fun levenshteinDistance(s1: String, s2: String): Int {
|
||||
@@ -52,8 +54,10 @@ class StringMatcher {
|
||||
val closestShowAndIndex = closestShow(target, shows)
|
||||
val closestIndex = closestShowAndIndex.second
|
||||
if (closestIndex == -1) {
|
||||
logger("No closest show found for $target")
|
||||
return shows // Return original list if no closest show found
|
||||
}
|
||||
logger("Closest show found for $target is ${closestShowAndIndex.first.name}")
|
||||
return listOf(shows[closestIndex]) + shows.subList(0, closestIndex) + shows.subList(closestIndex + 1, shows.size)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
package ani.dantotsu.settings
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
|
||||
import ani.dantotsu.loadData
|
||||
import com.bumptech.glide.Glide
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AnimeExtensionsFragment : Fragment(),
|
||||
SearchQueryHandler {
|
||||
private var _binding: FragmentAnimeExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
private lateinit var allextenstionsRecyclerView: RecyclerView
|
||||
private val animeExtensionManager: AnimeExtensionManager = Injekt.get<AnimeExtensionManager>()
|
||||
private val extensionsAdapter = AnimeExtensionsAdapter({ pkg ->
|
||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||
val context = requireContext() // Store context in a variable
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
|
||||
|
||||
if (pkg.hasUpdate) {
|
||||
animeExtensionManager.updateExtension(pkg)
|
||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle("Updating extension")
|
||||
.setContentText("Step: $installStep")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{ error ->
|
||||
Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle("Update failed")
|
||||
.setContentText("Error: ${error.message}")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
||||
.setContentTitle("Update complete")
|
||||
.setContentText("The extension has been successfully updated.")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
animeExtensionManager.uninstallExtension(pkg.pkgName)
|
||||
}
|
||||
}
|
||||
}, skipIcons)
|
||||
|
||||
private val allExtensionsAdapter = AllAnimeExtensionsAdapter(lifecycleScope, { pkgName ->
|
||||
|
||||
val notificationManager =
|
||||
requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
// Start the installation process
|
||||
animeExtensionManager.installExtension(pkgName)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
requireContext(),
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle("Installing extension")
|
||||
.setContentText("Step: $installStep")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{ error ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
requireContext(),
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle("Installation failed")
|
||||
.setContentText("Error: ${error.message}")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(
|
||||
requireContext(),
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
||||
.setContentTitle("Installation complete")
|
||||
.setContentText("The extension has been successfully installed.")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
}
|
||||
)
|
||||
}, skipIcons)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
|
||||
|
||||
extensionsRecyclerView = binding.animeExtensionsRecyclerView
|
||||
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
extensionsRecyclerView.adapter = extensionsAdapter
|
||||
|
||||
allextenstionsRecyclerView = binding.allAnimeExtensionsRecyclerView
|
||||
allextenstionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
allextenstionsRecyclerView.adapter = allExtensionsAdapter
|
||||
|
||||
lifecycleScope.launch {
|
||||
animeExtensionManager.installedExtensionsFlow.collect { extensions ->
|
||||
extensionsAdapter.updateData(extensions)
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
combine(
|
||||
animeExtensionManager.availableExtensionsFlow,
|
||||
animeExtensionManager.installedExtensionsFlow
|
||||
) { availableExtensions, installedExtensions ->
|
||||
// Pair of available and installed extensions
|
||||
Pair(availableExtensions, installedExtensions)
|
||||
}.collect { pair ->
|
||||
val (availableExtensions, installedExtensions) = pair
|
||||
|
||||
allExtensionsAdapter.updateData(availableExtensions, installedExtensions)
|
||||
}
|
||||
}
|
||||
val extensionsRecyclerView: RecyclerView = binding.animeExtensionsRecyclerView
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun updateContentBasedOnQuery(query: String?) {
|
||||
if (query.isNullOrEmpty()) {
|
||||
allExtensionsAdapter.filter("") // Reset the filter
|
||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
||||
extensionsRecyclerView.visibility = View.VISIBLE
|
||||
} else {
|
||||
allExtensionsAdapter.filter(query)
|
||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
||||
extensionsRecyclerView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView();_binding = null
|
||||
}
|
||||
|
||||
|
||||
private class AnimeExtensionsAdapter(
|
||||
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
|
||||
skipIcons: Boolean
|
||||
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
) {
|
||||
|
||||
val skipIcons = skipIcons
|
||||
|
||||
fun updateData(newExtensions: List<AnimeExtension.Installed>) {
|
||||
submitList(newExtensions) // Use submitList instead of manual list handling
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_extension, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
if (!skipIcons) {
|
||||
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
||||
}
|
||||
if (extension.hasUpdate) {
|
||||
holder.closeTextView.text = "Update"
|
||||
holder.closeTextView.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
holder.itemView.context,
|
||||
R.color.warning
|
||||
)
|
||||
)
|
||||
} else {
|
||||
holder.closeTextView.text = "Uninstall"
|
||||
}
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFF_CALLBACK_INSTALLED =
|
||||
object : DiffUtil.ItemCallback<AnimeExtension.Installed>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: AnimeExtension.Installed,
|
||||
newItem: AnimeExtension.Installed
|
||||
): Boolean {
|
||||
return oldItem.pkgName == newItem.pkgName
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: AnimeExtension.Installed,
|
||||
newItem: AnimeExtension.Installed
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class AllAnimeExtensionsAdapter(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val onButtonClicked: (AnimeExtension.Available) -> Unit,
|
||||
skipIcons: Boolean
|
||||
) : ListAdapter<AnimeExtension.Available, AllAnimeExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_AVAILABLE
|
||||
) {
|
||||
val skipIcons = skipIcons
|
||||
|
||||
fun updateData(
|
||||
newExtensions: List<AnimeExtension.Available>,
|
||||
installedExtensions: List<AnimeExtension.Installed> = emptyList()
|
||||
) {
|
||||
coroutineScope.launch(Dispatchers.Default) {
|
||||
val installedPkgNames = installedExtensions.map { it.pkgName }.toSet()
|
||||
val filteredExtensions = newExtensions.filter { it.pkgName !in installedPkgNames }
|
||||
|
||||
// Switch back to main thread to update UI
|
||||
withContext(Dispatchers.Main) {
|
||||
submitList(filteredExtensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): AllAnimeExtensionsAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_extension_all, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position)
|
||||
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
|
||||
if (!skipIcons) {
|
||||
Glide.with(holder.itemView.context)
|
||||
.load(extension.iconUrl)
|
||||
.into(holder.extensionIconImageView)
|
||||
}
|
||||
|
||||
holder.closeTextView.text = "Install"
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onButtonClicked(extension)
|
||||
}
|
||||
}
|
||||
|
||||
fun filter(query: String) {
|
||||
val filteredExtensions = if (query.isEmpty()) {
|
||||
currentList
|
||||
} else {
|
||||
currentList.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
submitList(filteredExtensions)
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFF_CALLBACK_AVAILABLE =
|
||||
object : DiffUtil.ItemCallback<AnimeExtension.Available>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: AnimeExtension.Available,
|
||||
newItem: AnimeExtension.Available
|
||||
): Boolean {
|
||||
return oldItem.pkgName == newItem.pkgName
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: AnimeExtension.Available,
|
||||
newItem: AnimeExtension.Available
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,92 +3,58 @@ 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
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
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.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
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 ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.MangaFragment
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class ExtensionsActivity : AppCompatActivity() {
|
||||
class ExtensionsActivity : AppCompatActivity() {
|
||||
private val restartMainActivity = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity)
|
||||
}
|
||||
lateinit var binding: ActivityExtensionsBinding
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
private lateinit var allextenstionsRecyclerView: RecyclerView
|
||||
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
|
||||
private val extensionsAdapter = ExtensionsAdapter { pkgName ->
|
||||
animeExtensionManager.uninstallExtension(pkgName)
|
||||
}
|
||||
private val allExtensionsAdapter = AllExtensionsAdapter(lifecycleScope) { pkgName ->
|
||||
|
||||
val notificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
// Start the installation process
|
||||
animeExtensionManager.installExtension(pkgName)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(this,
|
||||
ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle("Installing extension")
|
||||
.setContentText("Step: $installStep")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{ error ->
|
||||
val builder = NotificationCompat.Builder(this,
|
||||
ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle("Installation failed")
|
||||
.setContentText("Error: ${error.message}")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(this,
|
||||
ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_PROGRESS)
|
||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
||||
.setContentTitle("Installation complete")
|
||||
.setContentText("The extension has been successfully installed.")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@@ -97,35 +63,33 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
binding = ActivityExtensionsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
extensionsRecyclerView = findViewById(R.id.extensionsRecyclerView)
|
||||
extensionsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
extensionsRecyclerView.adapter = extensionsAdapter
|
||||
|
||||
allextenstionsRecyclerView = findViewById(R.id.allExtensionsRecyclerView)
|
||||
allextenstionsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
allextenstionsRecyclerView.adapter = allExtensionsAdapter
|
||||
val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
|
||||
val viewPager = findViewById<ViewPager2>(R.id.viewPager)
|
||||
|
||||
lifecycleScope.launch {
|
||||
animeExtensionManager.installedExtensionsFlow.collect { extensions ->
|
||||
extensionsAdapter.updateData(extensions)
|
||||
viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||
override fun getItemCount(): Int = 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> AnimeExtensionsFragment()
|
||||
1 -> MangaExtensionsFragment()
|
||||
else -> AnimeExtensionsFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
combine(
|
||||
animeExtensionManager.availableExtensionsFlow,
|
||||
animeExtensionManager.installedExtensionsFlow
|
||||
) { availableExtensions, installedExtensions ->
|
||||
// Pair of available and installed extensions
|
||||
Pair(availableExtensions, installedExtensions)
|
||||
}.collect { pair ->
|
||||
val (availableExtensions, installedExtensions) = pair
|
||||
allExtensionsAdapter.updateData(availableExtensions, installedExtensions)
|
||||
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||
tab.text = when (position) {
|
||||
0 -> "Anime" // Your tab title
|
||||
1 -> "Manga" // Your tab title
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}.attach()
|
||||
|
||||
|
||||
val searchView: SearchView = findViewById(R.id.searchView)
|
||||
val extensionsRecyclerView: RecyclerView = findViewById(R.id.extensionsRecyclerView)
|
||||
|
||||
val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
@@ -133,17 +97,11 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
if (newText.isNullOrEmpty()) {
|
||||
allExtensionsAdapter.filter("") // Reset the filter
|
||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
||||
extensionsHeader.visibility = View.VISIBLE
|
||||
extensionsRecyclerView.visibility = View.VISIBLE
|
||||
} else {
|
||||
allExtensionsAdapter.filter(newText)
|
||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
||||
extensionsRecyclerView.visibility = View.GONE
|
||||
extensionsHeader.visibility = View.GONE
|
||||
val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}")
|
||||
if (currentFragment is SearchQueryHandler) {
|
||||
currentFragment.updateContentBasedOnQuery(newText)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
@@ -164,104 +122,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()
|
||||
|
||||
fun updateData(newExtensions: List<AnimeExtension.Installed>) {
|
||||
extensions = newExtensions
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_extension, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = extensions[position]
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
||||
holder.closeTextView.text = "Uninstall"
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension.pkgName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = extensions.size
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||
}
|
||||
}
|
||||
|
||||
private class AllExtensionsAdapter(private val coroutineScope: CoroutineScope,
|
||||
private val onButtonClicked: (AnimeExtension.Available) -> Unit) : RecyclerView.Adapter<AllExtensionsAdapter.ViewHolder>() {
|
||||
private var extensions: List<AnimeExtension.Available> = emptyList()
|
||||
|
||||
fun updateData(newExtensions: List<AnimeExtension.Available>, installedExtensions: List<AnimeExtension.Installed> = emptyList()) {
|
||||
val installedPkgNames = installedExtensions.map { it.pkgName }.toSet()
|
||||
extensions = newExtensions.filter { it.pkgName !in installedPkgNames }
|
||||
filteredExtensions = extensions
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AllExtensionsAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_extension_all, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = filteredExtensions[position]
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
coroutineScope.launch {
|
||||
val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl)
|
||||
holder.extensionIconImageView.setImageDrawable(drawable)
|
||||
}
|
||||
holder.closeTextView.text = "Install"
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onButtonClicked(extension)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = filteredExtensions.size
|
||||
|
||||
private var filteredExtensions: List<AnimeExtension.Available> = emptyList()
|
||||
|
||||
fun filter(query: String) {
|
||||
filteredExtensions = if (query.isEmpty()) {
|
||||
extensions
|
||||
} else {
|
||||
extensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||
}
|
||||
|
||||
suspend fun urlToDrawable(context: Context, url: String): Drawable? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
return@withContext Glide.with(context)
|
||||
.load(url)
|
||||
.submit()
|
||||
.get()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
interface SearchQueryHandler {
|
||||
fun updateContentBasedOnQuery(query: String?)
|
||||
}
|
||||
|
||||
@@ -11,26 +11,96 @@ import ani.dantotsu.initActivity
|
||||
class FAQActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityFaqBinding
|
||||
|
||||
private val faqs = listOf(
|
||||
private val faqs by lazy {
|
||||
listOf(
|
||||
|
||||
Triple(R.drawable.ic_round_help_24, currContext()!!.getString(R.string.question_1), currContext()!!.getString(R.string.answer_1)),
|
||||
Triple(R.drawable.ic_round_auto_awesome_24, currContext()!!.getString(R.string.question_2), currContext()!!.getString(R.string.answer_2)),
|
||||
Triple(R.drawable.ic_round_auto_awesome_24, currContext()!!.getString(R.string.question_17), currContext()!!.getString(R.string.answer_17)),
|
||||
Triple(R.drawable.ic_round_download_24, currContext()!!.getString(R.string.question_3), currContext()!!.getString(R.string.answer_3)),
|
||||
Triple(R.drawable.ic_round_help_24, currContext()!!.getString(R.string.question_16), currContext()!!.getString(R.string.answer_16)),
|
||||
Triple(R.drawable.ic_round_dns_24, currContext()!!.getString(R.string.question_4), currContext()!!.getString(R.string.answer_4)),
|
||||
Triple(R.drawable.ic_baseline_screen_lock_portrait_24, currContext()!!.getString(R.string.question_5), currContext()!!.getString(R.string.answer_5)),
|
||||
Triple(R.drawable.ic_anilist, currContext()!!.getString(R.string.question_6), currContext()!!.getString(R.string.answer_6)),
|
||||
Triple(R.drawable.ic_round_movie_filter_24, currContext()!!.getString(R.string.question_7), currContext()!!.getString(R.string.answer_7)),
|
||||
Triple(R.drawable.ic_round_menu_book_24, currContext()!!.getString(R.string.question_8), currContext()!!.getString(R.string.answer_8)),
|
||||
Triple(R.drawable.ic_round_lock_open_24, currContext()!!.getString(R.string.question_9), currContext()!!.getString(R.string.answer_9)),
|
||||
Triple(R.drawable.ic_round_smart_button_24, currContext()!!.getString(R.string.question_10), currContext()!!.getString(R.string.answer_10)),
|
||||
Triple(R.drawable.ic_round_smart_button_24, currContext()!!.getString(R.string.question_11), currContext()!!.getString(R.string.answer_11)),
|
||||
Triple(R.drawable.ic_round_info_24, currContext()!!.getString(R.string.question_12), currContext()!!.getString(R.string.answer_12)),
|
||||
Triple(R.drawable.ic_round_help_24, currContext()!!.getString(R.string.question_13), currContext()!!.getString(R.string.answer_13)),
|
||||
Triple(R.drawable.ic_round_art_track_24, currContext()!!.getString(R.string.question_14), currContext()!!.getString(R.string.answer_14)),
|
||||
Triple(R.drawable.ic_round_video_settings_24, currContext()!!.getString(R.string.question_15), currContext()!!.getString(R.string.answer_15))
|
||||
Triple(
|
||||
R.drawable.ic_round_help_24,
|
||||
currContext()!!.getString(R.string.question_1),
|
||||
currContext()!!.getString(R.string.answer_1)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_auto_awesome_24,
|
||||
currContext()!!.getString(R.string.question_2),
|
||||
currContext()!!.getString(R.string.answer_2)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_auto_awesome_24,
|
||||
currContext()!!.getString(R.string.question_17),
|
||||
currContext()!!.getString(R.string.answer_17)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_download_24,
|
||||
currContext()!!.getString(R.string.question_3),
|
||||
currContext()!!.getString(R.string.answer_3)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_help_24,
|
||||
currContext()!!.getString(R.string.question_16),
|
||||
currContext()!!.getString(R.string.answer_16)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_dns_24,
|
||||
currContext()!!.getString(R.string.question_4),
|
||||
currContext()!!.getString(R.string.answer_4)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_baseline_screen_lock_portrait_24,
|
||||
currContext()!!.getString(R.string.question_5),
|
||||
currContext()!!.getString(R.string.answer_5)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_anilist,
|
||||
currContext()!!.getString(R.string.question_6),
|
||||
currContext()!!.getString(R.string.answer_6)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_movie_filter_24,
|
||||
currContext()!!.getString(R.string.question_7),
|
||||
currContext()!!.getString(R.string.answer_7)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_menu_book_24,
|
||||
currContext()!!.getString(R.string.question_8),
|
||||
currContext()!!.getString(R.string.answer_8)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_lock_open_24,
|
||||
currContext()!!.getString(R.string.question_9),
|
||||
currContext()!!.getString(R.string.answer_9)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_smart_button_24,
|
||||
currContext()!!.getString(R.string.question_10),
|
||||
currContext()!!.getString(R.string.answer_10)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_smart_button_24,
|
||||
currContext()!!.getString(R.string.question_11),
|
||||
currContext()!!.getString(R.string.answer_11)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_info_24,
|
||||
currContext()!!.getString(R.string.question_12),
|
||||
currContext()!!.getString(R.string.answer_12)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_help_24,
|
||||
currContext()!!.getString(R.string.question_13),
|
||||
currContext()!!.getString(R.string.answer_13)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_art_track_24,
|
||||
currContext()!!.getString(R.string.question_14),
|
||||
currContext()!!.getString(R.string.answer_14)
|
||||
),
|
||||
Triple(
|
||||
R.drawable.ic_round_video_settings_24,
|
||||
currContext()!!.getString(R.string.question_15),
|
||||
currContext()!!.getString(R.string.answer_15)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
package ani.dantotsu.settings
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
||||
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
||||
import ani.dantotsu.loadData
|
||||
import com.bumptech.glide.Glide
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaExtensionsFragment : Fragment(),
|
||||
SearchQueryHandler {
|
||||
private var _binding: FragmentMangaExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
private lateinit var allextenstionsRecyclerView: RecyclerView
|
||||
private val mangaExtensionManager: MangaExtensionManager = Injekt.get<MangaExtensionManager>()
|
||||
private val extensionsAdapter = MangaExtensionsAdapter({ pkg ->
|
||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||
val context = requireContext() // Store context in a variable
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
|
||||
|
||||
if (pkg.hasUpdate) {
|
||||
mangaExtensionManager.updateExtension(pkg)
|
||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle("Updating extension")
|
||||
.setContentText("Step: $installStep")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{ error ->
|
||||
Log.e("MangaExtensionsAdapter", "Error: ", error) // Log the error
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle("Update failed")
|
||||
.setContentText("Error: ${error.message}")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
||||
.setContentTitle("Update complete")
|
||||
.setContentText("The extension has been successfully updated.")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
mangaExtensionManager.uninstallExtension(pkg.pkgName)
|
||||
}
|
||||
}
|
||||
}, skipIcons)
|
||||
|
||||
private val allExtensionsAdapter =
|
||||
AllMangaExtensionsAdapter(lifecycleScope, { pkgName ->
|
||||
|
||||
val notificationManager =
|
||||
requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
// Start the installation process
|
||||
mangaExtensionManager.installExtension(pkgName)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
requireContext(),
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle("Installing extension")
|
||||
.setContentText("Step: $installStep")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{ error ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
requireContext(),
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle("Installation failed")
|
||||
.setContentText("Error: ${error.message}")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(
|
||||
requireContext(),
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
||||
.setContentTitle("Installation complete")
|
||||
.setContentText("The extension has been successfully installed.")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
}
|
||||
)
|
||||
}, skipIcons)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
|
||||
|
||||
extensionsRecyclerView = binding.mangaExtensionsRecyclerView
|
||||
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
extensionsRecyclerView.adapter = extensionsAdapter
|
||||
|
||||
allextenstionsRecyclerView = binding.allMangaExtensionsRecyclerView
|
||||
allextenstionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
allextenstionsRecyclerView.adapter = allExtensionsAdapter
|
||||
|
||||
lifecycleScope.launch {
|
||||
mangaExtensionManager.installedExtensionsFlow.collect { extensions ->
|
||||
extensionsAdapter.updateData(extensions)
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
combine(
|
||||
mangaExtensionManager.availableExtensionsFlow,
|
||||
mangaExtensionManager.installedExtensionsFlow
|
||||
) { availableExtensions, installedExtensions ->
|
||||
// Pair of available and installed extensions
|
||||
Pair(availableExtensions, installedExtensions)
|
||||
}.collect { pair ->
|
||||
val (availableExtensions, installedExtensions) = pair
|
||||
allExtensionsAdapter.updateData(availableExtensions, installedExtensions)
|
||||
}
|
||||
}
|
||||
val extensionsRecyclerView: RecyclerView = binding.mangaExtensionsRecyclerView
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun updateContentBasedOnQuery(query: String?) {
|
||||
if (query.isNullOrEmpty()) {
|
||||
allExtensionsAdapter.filter("") // Reset the filter
|
||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
||||
extensionsRecyclerView.visibility = View.VISIBLE
|
||||
} else {
|
||||
allExtensionsAdapter.filter(query)
|
||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
||||
extensionsRecyclerView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView();_binding = null
|
||||
}
|
||||
|
||||
private class MangaExtensionsAdapter(
|
||||
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
|
||||
skipIcons: Boolean
|
||||
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
) {
|
||||
|
||||
val skipIcons = skipIcons
|
||||
|
||||
// Use submitList to update data
|
||||
fun updateData(newExtensions: List<MangaExtension.Installed>) {
|
||||
submitList(newExtensions)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_extension, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position) // Use getItem from ListAdapter
|
||||
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
if (!skipIcons) {
|
||||
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
holder.closeTextView.text = "Update"
|
||||
holder.closeTextView.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
holder.itemView.context,
|
||||
R.color.warning
|
||||
)
|
||||
)
|
||||
} else {
|
||||
holder.closeTextView.text = "Uninstall"
|
||||
}
|
||||
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFF_CALLBACK_INSTALLED =
|
||||
object : DiffUtil.ItemCallback<MangaExtension.Installed>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: MangaExtension.Installed,
|
||||
newItem: MangaExtension.Installed
|
||||
): Boolean {
|
||||
return oldItem.pkgName == newItem.pkgName
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: MangaExtension.Installed,
|
||||
newItem: MangaExtension.Installed
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class AllMangaExtensionsAdapter(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val onButtonClicked: (MangaExtension.Available) -> Unit,
|
||||
skipIcons: Boolean
|
||||
) : ListAdapter<MangaExtension.Available, AllMangaExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_AVAILABLE
|
||||
) {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
|
||||
val skipIcons = skipIcons
|
||||
|
||||
// Use submitList to update the data
|
||||
fun updateData(
|
||||
newExtensions: List<MangaExtension.Available>,
|
||||
installedExtensions: List<MangaExtension.Installed> = emptyList()
|
||||
) {
|
||||
coroutineScope.launch(Dispatchers.Default) {
|
||||
val installedPkgNames = installedExtensions.map { it.pkgName }.toSet()
|
||||
val filteredExtensions = newExtensions.filter { it.pkgName !in installedPkgNames }
|
||||
|
||||
// Switch back to main thread to update UI
|
||||
withContext(Dispatchers.Main) {
|
||||
submitList(filteredExtensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_extension_all, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position) // Use getItem from ListAdapter
|
||||
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
if (!skipIcons) {
|
||||
Glide.with(holder.itemView.context)
|
||||
.load(extension.iconUrl)
|
||||
.into(holder.extensionIconImageView)
|
||||
}
|
||||
|
||||
holder.closeTextView.text = "Install"
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onButtonClicked(extension)
|
||||
}
|
||||
}
|
||||
|
||||
// Filtering function
|
||||
fun filter(query: String) {
|
||||
val filteredExtensions = if (query.isEmpty()) {
|
||||
currentList
|
||||
} else {
|
||||
currentList.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
submitList(filteredExtensions)
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFF_CALLBACK_AVAILABLE =
|
||||
object : DiffUtil.ItemCallback<MangaExtension.Available>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: MangaExtension.Available,
|
||||
newItem: MangaExtension.Available
|
||||
): Boolean {
|
||||
return oldItem.pkgName == newItem.pkgName
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: MangaExtension.Available,
|
||||
newItem: MangaExtension.Available
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package ani.dantotsu.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Build.*
|
||||
@@ -30,11 +31,15 @@ import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
@@ -43,6 +48,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||
override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity)
|
||||
}
|
||||
lateinit var binding: ActivitySettingsBinding
|
||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
||||
private val networkPreferences = Injekt.get<NetworkPreferences>()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -88,14 +95,23 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
val animeSource = loadData<Int>("settings_def_anime_source")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0
|
||||
if (MangaSources.names.isNotEmpty() && animeSource in 0 until MangaSources.names.size) {
|
||||
binding.mangaSource.setText(MangaSources.names[animeSource], false)
|
||||
binding.settingsUseMaterialYou.isChecked = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false)
|
||||
binding.settingsUseMaterialYou.setOnCheckedChangeListener { _, isChecked ->
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_material_you", isChecked).apply()
|
||||
}
|
||||
|
||||
//val animeSource = loadData<Int>("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0
|
||||
val animeSource = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("settings_def_anime_source_s_r", 0)
|
||||
if (AnimeSources.names.isNotEmpty() && animeSource in 0 until AnimeSources.names.size) {
|
||||
binding.animeSource.setText(AnimeSources.names[animeSource], false)
|
||||
|
||||
}
|
||||
|
||||
binding.animeSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, AnimeSources.names))
|
||||
|
||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||
saveData("settings_def_anime_source", i)
|
||||
//saveData("settings_def_anime_source_s", i)
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("settings_def_anime_source_s_r", i).apply()
|
||||
binding.animeSource.clearFocus()
|
||||
}
|
||||
|
||||
@@ -114,6 +130,26 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
}.show()
|
||||
}
|
||||
|
||||
binding.settingsForceLegacyInstall.isChecked = extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY
|
||||
binding.settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
extensionInstaller.set(BasePreferences.ExtensionInstaller.LEGACY)
|
||||
}else{
|
||||
extensionInstaller.set(BasePreferences.ExtensionInstaller.PACKAGEINSTALLER)
|
||||
}
|
||||
}
|
||||
|
||||
binding.skipExtensionIcons.isChecked = loadData("skip_extension_icons") ?: false
|
||||
binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked ->
|
||||
saveData("skip_extension_icons", isChecked)
|
||||
}
|
||||
|
||||
binding.userAgent.setText(networkPreferences.defaultUserAgent().get())
|
||||
binding.userAgent.setOnEditorActionListener { _, _, _ ->
|
||||
networkPreferences.defaultUserAgent().set(binding.userAgent.text.toString())
|
||||
true
|
||||
}
|
||||
|
||||
binding.settingsDownloadInSd.isChecked = loadData("sd_dl") ?: false
|
||||
binding.settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
@@ -152,14 +188,19 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
saveData("settings_prefer_dub", isChecked)
|
||||
}
|
||||
|
||||
val mangaSource = loadData<Int>("settings_def_manga_source")?.let { if (it >= MangaSources.names.size) 0 else it } ?: 0
|
||||
if (MangaSources.names.isNotEmpty() && mangaSource in 0 until MangaSources.names.size) {
|
||||
binding.mangaSource.setText(MangaSources.names[mangaSource], false)
|
||||
//val mangaSource = loadData<Int>("settings_def_manga_source_s")?.let { if (it >= MangaSources.names.size) 0 else it } ?: 0
|
||||
val mangaSource = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("settings_def_manga_source_s_r", 0)
|
||||
if (MangaSources.names.isNotEmpty() && mangaSource in 0 until MangaSources.names.size) {
|
||||
binding.mangaSource.setText(MangaSources.names[mangaSource], false)
|
||||
}
|
||||
|
||||
// Set up the dropdown adapter.
|
||||
binding.mangaSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, MangaSources.names))
|
||||
|
||||
// Set up the item click listener for the dropdown.
|
||||
binding.mangaSource.setOnItemClickListener { _, _, i, _ ->
|
||||
saveData("settings_def_manga_source", i)
|
||||
//saveData("settings_def_manga_source_s", i)
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("settings_def_manga_source_s_r", i).apply()
|
||||
binding.mangaSource.clearFocus()
|
||||
}
|
||||
|
||||
@@ -497,7 +538,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
title = "Enjoying the App?"
|
||||
addView(TextView(this@SettingsActivity).apply {
|
||||
text =
|
||||
"Consider donating!\nOnce we reach the goal of $1000 (60%+ already reached!), Get ready to get an Offline Player & Manga Downloads!"
|
||||
"Consider donating!r"
|
||||
})
|
||||
|
||||
setNegativeButton("no moners :(") {
|
||||
|
||||
@@ -14,14 +14,16 @@ import kotlinx.coroutines.withTimeoutOrNull
|
||||
class SubscriptionHelper {
|
||||
companion object {
|
||||
private fun loadSelected(context: Context, mediaId: Int, isAdult: Boolean, isAnime: Boolean): Selected {
|
||||
return loadData<Selected>("${mediaId}-select", context) ?: Selected().let {
|
||||
it.source =
|
||||
val sharedPreferences = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
val data = loadData<Selected>("${mediaId}-select", context) ?: Selected().let {
|
||||
it.sourceIndex =
|
||||
if (isAdult) 0
|
||||
else if (isAnime) loadData("settings_def_anime_source", context) ?: 0
|
||||
else loadData("settings_def_manga_source", context) ?: 0
|
||||
else if (isAnime) {sharedPreferences.getInt("settings_def_anime_source_s_r",0)}
|
||||
else {sharedPreferences.getInt("settings_def_manga_source_s_r",0)}
|
||||
it.preferDub = loadData("settings_prefer_dub", context) ?: false
|
||||
it
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private fun saveSelected(context: Context, mediaId: Int, data: Selected) {
|
||||
@@ -31,7 +33,7 @@ class SubscriptionHelper {
|
||||
fun getAnimeParser(context: Context, isAdult: Boolean, id: Int): AnimeParser {
|
||||
val sources = if (isAdult) HAnimeSources else AnimeSources
|
||||
val selected = loadSelected(context, id, isAdult, true)
|
||||
val parser = sources[selected.source]
|
||||
val parser = sources[selected.sourceIndex]
|
||||
parser.selectDub = selected.preferDub
|
||||
return parser
|
||||
}
|
||||
@@ -58,7 +60,7 @@ class SubscriptionHelper {
|
||||
fun getMangaParser(context: Context, isAdult: Boolean, id: Int): MangaParser {
|
||||
val sources = if (isAdult) HMangaSources else MangaSources
|
||||
val selected = loadSelected(context, id, isAdult, false)
|
||||
return sources[selected.source]
|
||||
return sources[selected.sourceIndex]
|
||||
}
|
||||
|
||||
suspend fun getChapter(context: Context, parser: MangaParser, id: Int, isAdult: Boolean): MangaChapter? {
|
||||
@@ -66,7 +68,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,11 +1,12 @@
|
||||
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
|
||||
import rx.subjects.Subject
|
||||
import java.io.Serializable
|
||||
|
||||
data class Track(val url: String, val lang: String)
|
||||
|
||||
@@ -17,7 +18,7 @@ open class Video(
|
||||
// "url", "language-label-2", "url2", "language-label-2"
|
||||
val subtitleTracks: List<Track> = emptyList(),
|
||||
val audioTracks: List<Track> = emptyList(),
|
||||
) : ProgressListener {
|
||||
) : Serializable, ProgressListener {
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
constructor(
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
@@ -130,7 +130,7 @@ internal object AnimeExtensionLoader {
|
||||
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||
logcat(LogPriority.WARN) {
|
||||
"Lib version is $libVersion, while only versions " +
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||
}
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
@@ -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,21 +1,23 @@
|
||||
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
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NetworkHelper(
|
||||
context: Context,
|
||||
private val preferences: NetworkPreferences,
|
||||
) {
|
||||
|
||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||
@@ -40,18 +42,17 @@ class NetworkHelper(
|
||||
.addInterceptor(UncaughtExceptionInterceptor())
|
||||
.addInterceptor(userAgentInterceptor)
|
||||
|
||||
/*if (preferences.verboseLogging().get()) {
|
||||
if (preferences.verboseLogging().get()) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addNetworkInterceptor(httpLoggingInterceptor)
|
||||
}*/
|
||||
}
|
||||
|
||||
//when (preferences.dohProvider().get()) {
|
||||
when (PREF_DOH_CLOUDFLARE) {
|
||||
when (preferences.dohProvider().get()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
/*PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||
PREF_DOH_QUAD9 -> builder.dohQuad9()
|
||||
PREF_DOH_ALIDNS -> builder.dohAliDNS()
|
||||
PREF_DOH_DNSPOD -> builder.dohDNSPod()
|
||||
@@ -60,7 +61,7 @@ class NetworkHelper(
|
||||
PREF_DOH_MULLVAD -> builder.dohMullvad()
|
||||
PREF_DOH_CONTROLD -> builder.dohControlD()
|
||||
PREF_DOH_NJALLA -> builder.dohNajalla()
|
||||
PREF_DOH_SHECAN -> builder.dohShecan()*/
|
||||
PREF_DOH_SHECAN -> builder.dohShecan()
|
||||
}
|
||||
|
||||
return builder
|
||||
@@ -75,5 +76,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() = preferences.defaultUserAgent().get().trim()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class NetworkPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
private val verboseLogging: Boolean = false,
|
||||
) {
|
||||
|
||||
fun verboseLogging(): Preference<Boolean> {
|
||||
return preferenceStore.getBoolean("verbose_logging", verboseLogging)
|
||||
}
|
||||
|
||||
fun dohProvider(): Preference<Int> {
|
||||
return preferenceStore.getInt("doh_provider", 1)
|
||||
}
|
||||
|
||||
fun defaultUserAgent(): Preference<String> {
|
||||
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user