mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-17 11:43:55 +00:00
Compare commits
58 Commits
v0.1.5
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d9254140d | ||
|
|
533aa9f56e | ||
|
|
c310bea0e9 | ||
|
|
813f7a0992 | ||
|
|
4c0f56d3e3 | ||
|
|
1f44d32f35 | ||
|
|
d937f447ef | ||
|
|
187262a266 | ||
|
|
f40ebc9d09 | ||
|
|
3998d88297 | ||
|
|
4db301ca7a | ||
|
|
3dfcc9fc31 | ||
|
|
df63586c02 | ||
|
|
d7372d4dbb | ||
|
|
f4266d0da3 | ||
|
|
736b06bdbe | ||
|
|
5543d29317 | ||
|
|
2fc351f57a | ||
|
|
eee1242964 | ||
|
|
a58e9a523a | ||
|
|
cd3aad1c33 | ||
|
|
5a482d8307 | ||
|
|
91d869005c | ||
|
|
05e73269d3 | ||
|
|
3dac48ced8 | ||
|
|
8c5726ab8a | ||
|
|
076516be23 | ||
|
|
1059a3c17e | ||
|
|
c75df942f2 | ||
|
|
cfe7be5cdb | ||
|
|
390fc18c4c | ||
|
|
4c82c56828 | ||
|
|
da5c480ba7 | ||
|
|
20acd71b1a | ||
|
|
231c9c5b98 | ||
|
|
4cfdcdb23c | ||
|
|
acb0225699 | ||
|
|
ebffaaa742 | ||
|
|
1760064555 | ||
|
|
44a6db3fc2 | ||
|
|
aab25d157e | ||
|
|
8a4be86ddc | ||
|
|
31baf729be | ||
|
|
b98e3dc780 | ||
|
|
878d58679e | ||
|
|
f500ba6cf0 | ||
|
|
d124736556 | ||
|
|
1a825e2509 | ||
|
|
6e14c2221d | ||
|
|
5b6e351a56 | ||
|
|
c310708401 | ||
|
|
f0093b903a | ||
|
|
d33568f0ad | ||
|
|
26f9f40042 | ||
|
|
7545870f38 | ||
|
|
3368a1bc8d | ||
|
|
9c0ef7a788 | ||
|
|
960c2b4113 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,3 +28,6 @@ google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
#other
|
||||
scripts/
|
||||
|
||||
113
README.md
113
README.md
@@ -1,109 +1,36 @@
|
||||
# **Dantotsu** (🚧 ALPHA 🚧)
|
||||
|
||||
> ⚠️ **WARNING**: This project is in alpha stage. Things may not work as expected.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://pbxt.replicate.delivery/2PX94viD6lJSDVayQrGyDH7CGu7IjQ6e8HEtOGDeelefXRdOC/out.png" alt="Dantotsu Banner" width=100% >
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
|
||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
||||
<a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
|
||||
</p>
|
||||
Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-of-the-art elegance. It is an <a href="https://anilist.co/">Anilist</a> only client, which also lets you stream-download Anime & Manga through extensions.
|
||||
<br><br>
|
||||
<i>Dantotsu (断トツ; Dan-totsu) literally means the best of the best in Japanese. Well, we would like to say this is the best open source app for anime and manga on Android, but hey, try it out yourself & judge!
|
||||
</i>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
# **Dantotsu** 🌟
|
||||
|
||||
Dantotsu is an [Anilist](https://anilist.co/) only client.
|
||||
|
||||
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||
<br>
|
||||
|
||||
### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> Please do not attempt to upload Dantotsu or any of it's forks on Playstore or any other Android appstores on the internet. Doing so, may infringe their terms and conditions. This may result to legal action or immediate take-down of the app.
|
||||
## WANT TO CONTRIBUTE? 🤝
|
||||
|
||||
## Extension Status
|
||||
All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content - whatever you have to offer, we can use!
|
||||
|
||||
| Type | Status |
|
||||
| ---------------- | ------- |
|
||||
| Anime Extensions | Working |
|
||||
| Manga Extensions | "Working" |
|
||||
| Light Novel Extensions | Not Working |
|
||||
You can come hang out with our awesome community, request new features, and report any bugs or issues at our Discord server too. 📣
|
||||
|
||||
### OFFICIAL DISCORD SERVER 🚀
|
||||
|
||||
|
||||
## APP FEATURES
|
||||
|
||||
- Easy and functional way to both, watch anime and read manga, ad-free.
|
||||
|
||||
- A completely open source app with a nice UI & Animations :)
|
||||
|
||||
- Aniyomi extension support built right into the app.
|
||||
|
||||
- Synchronize anime and manga real-time with AniList and MyAnimeList. Easily categorise anime and manga based on your current status. (Powered by AniList)
|
||||
|
||||
- Find all shows using thoroughly and frequently updated list of all trending, popular and ongoing anime based on scores.
|
||||
|
||||
- View extensive details about anime shows, movies and manga titles. It also features ability to countdown to the next episode of airing anime. (Powered by AniList & MyAnimeList)
|
||||
|
||||
- Get notified when new episodes/chapters are released!
|
||||
|
||||
|
||||
* **Available Anime sources:-**
|
||||
NONE BUILT IN!
|
||||
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
|
||||
|
||||
|
||||
* **Available Manga sources:-**
|
||||
NONE BUILT IN!
|
||||
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
|
||||
|
||||
## Planned Stuff
|
||||
|
||||
- get app out of alpha
|
||||
|
||||
- Accent Color Change (RIP Hot Pink Supremacy.)
|
||||
|
||||
|
||||
## Rejected Stuff (still rejected)
|
||||
|
||||
- Sources of any language except English
|
||||
|
||||
- News Section in the App
|
||||
|
||||
- Comment Section
|
||||
|
||||
|
||||
## WANT TO CONTRIBUTE?
|
||||
|
||||
- All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content- whatever you have to offer, we can use it!
|
||||
|
||||
- You can come hang out with our awesome community and request new features and report any bugs or issue at our discord server too.
|
||||
|
||||
### Official Discord Server
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
||||
<a href="https://discord.gg/4HPZ5nAWwM">
|
||||
<img src="https://invidget.switchblade.xyz/4HPZ5nAWwM">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
### VISIT FOR MORE INFORMATION:-
|
||||
|
||||
no website yet :(
|
||||
|
||||
## DISCLAIMER
|
||||
|
||||
* Dantotsu by itself only provides an anime and manga tracker and does not provide any anime or manga streaming or downloading capabilities.
|
||||
|
||||
* Dantotsu or any of its developer/staff don't host any of the content found inside Dantotsu. Any and all images and anime/manga information found in the app are taken from various public APIs (AniList, MyAnimeList, Kitsu).
|
||||
|
||||
* Furthermore, all of the anime/manga links found in Dantotsu are taken from various 3rd party plugins and have no affiliation with Dantotsu or its staff.
|
||||
|
||||
* Dantotsu or it's owners aren't liable for any misuse of any of the contents found inside or outside of the app and cannot be held accountable for the distribution of any of the contents found inside the app.
|
||||
|
||||
* By using Dantotsu, you comply to the fact that the developer of the app is not responsible for any of the contents found in the app. You also agree to the fact that you may not use Dantotsu to download or stream any copyrighted content.
|
||||
|
||||
* If the internet infringement issues are involved, please contact the source website. The developer does not assume any legal responsibility.
|
||||
|
||||
## License
|
||||
## LICENSE 📜
|
||||
|
||||
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
||||
|
||||
@@ -21,15 +21,14 @@ android {
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "0.1.5"
|
||||
versionName "1.0.0-beta03"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
//applicationIdSuffix ".beta"
|
||||
applicationIdSuffix ".beta"
|
||||
debuggable true
|
||||
versionNameSuffix "." + gitCommitHash
|
||||
}
|
||||
release {
|
||||
debuggable false
|
||||
@@ -62,9 +61,10 @@ dependencies {
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
|
||||
// Glide
|
||||
ext.glide_version = '4.16.0'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dantotsu α</string>
|
||||
<string name="app_name">Dantotsu</string>
|
||||
</resources>
|
||||
@@ -221,6 +221,7 @@
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".download.DownloadContainerActivity" />
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
@@ -263,10 +264,16 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".download.manga.MangaDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -11,16 +11,28 @@ import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import ani.dantotsu.others.DisabledReports
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Locale
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class App : MultiDexApplication() {
|
||||
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
@@ -52,6 +64,22 @@ class App : MultiDexApplication() {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
}
|
||||
|
||||
animeExtensionManager = Injekt.get()
|
||||
mangaExtensionManager = Injekt.get()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setupNotificationChannels() {
|
||||
|
||||
@@ -188,6 +188,9 @@ fun Activity.hideStatusBar() {
|
||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val window = dialog?.window
|
||||
val decorView: View = window?.decorView ?: return
|
||||
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
@@ -204,23 +207,21 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
fun isOnline(context: Context): Boolean {
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
return tryWith {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return@tryWith if (cap != null) {
|
||||
when {
|
||||
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
||||
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
||||
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
||||
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
||||
cap.hasTransport(TRANSPORT_USB) ||
|
||||
cap.hasTransport(TRANSPORT_VPN) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return@tryWith if (cap != null) {
|
||||
when {
|
||||
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
|
||||
cap.hasTransport(TRANSPORT_CELLULAR) ||
|
||||
cap.hasTransport(TRANSPORT_ETHERNET) ||
|
||||
cap.hasTransport(TRANSPORT_LOWPAN) ||
|
||||
cap.hasTransport(TRANSPORT_USB) ||
|
||||
cap.hasTransport(TRANSPORT_VPN) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI) ||
|
||||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
} else true
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
} ?: false
|
||||
}
|
||||
|
||||
@@ -419,7 +420,13 @@ fun String.findBetween(a: String, b: String): String? {
|
||||
|
||||
fun ImageView.loadImage(url: String?, size: Int = 0) {
|
||||
if (!url.isNullOrEmpty()) {
|
||||
loadImage(FileUrl(url), size)
|
||||
val localFile = File(url)
|
||||
if (localFile.exists()) {
|
||||
loadLocalImage(localFile, size)
|
||||
}
|
||||
else {
|
||||
loadImage(FileUrl(url), size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +439,14 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
|
||||
if (file?.exists() == true) {
|
||||
tryWith {
|
||||
Glide.with(this.context).load(file).transition(withCrossFade()).override(size).into(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SafeClickListener(
|
||||
private var defaultInterval: Int = 1000,
|
||||
private val onSafeCLick: (View) -> Unit
|
||||
@@ -729,7 +744,7 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG)
|
||||
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_SHORT)
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
@@ -40,7 +41,9 @@ import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.ItemNavbarBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
@@ -54,6 +57,8 @@ import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
@@ -76,39 +81,32 @@ class MainActivity : AppCompatActivity() {
|
||||
private var load = false
|
||||
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
|
||||
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
val backgroundDrawable = bottomBar.background as GradientDrawable
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
bottomBar.background = backgroundDrawable
|
||||
_bottomBar.background = backgroundDrawable
|
||||
}
|
||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (doubleBackToExitPressedOnce) {
|
||||
@@ -125,24 +123,40 @@ class MainActivity : AppCompatActivity() {
|
||||
binding.root.isMotionEventSplittingEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||
binding.root.addView(splash.root)
|
||||
(splash.splashImage.drawable as Animatable).start()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||
binding.root.addView(splash.root)
|
||||
(splash.splashImage.drawable as Animatable).start()
|
||||
|
||||
// Wait for 2 seconds (2000 milliseconds)
|
||||
delay(2000)
|
||||
delay(1200)
|
||||
|
||||
// Now perform the animation
|
||||
ObjectAnimator.ofFloat(
|
||||
splash.root,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splash.root.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { binding.root.removeView(splash.root) }
|
||||
start()
|
||||
ObjectAnimator.ofFloat(
|
||||
splash.root,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splash.root.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { binding.root.removeView(splash.root) }
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
splashScreen.setOnExitAnimationListener { splashScreenView ->
|
||||
ObjectAnimator.ofFloat(
|
||||
splashScreenView,
|
||||
View.TRANSLATION_Y,
|
||||
0f,
|
||||
-splashScreenView.height.toFloat()
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd { splashScreenView.remove() }
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +165,7 @@ class MainActivity : AppCompatActivity() {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
binding.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
@@ -164,7 +178,7 @@ class MainActivity : AppCompatActivity() {
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.navbar
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
|
||||
@@ -3,6 +3,7 @@ package ani.dantotsu.aniyomi.anime.custom
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
@@ -12,23 +13,32 @@ 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 eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingleton
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { DownloadsManager(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app, get()) }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
|
||||
addSingletonFactory { MangaExtensionManager(app) }
|
||||
|
||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||
|
||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
addSingleton(sharedPreferences)
|
||||
|
||||
@@ -40,6 +50,11 @@ class AppModule(val app: Application) : InjektModule {
|
||||
}
|
||||
|
||||
addSingletonFactory { MangaCache() }
|
||||
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
get<AnimeSourceManager>()
|
||||
get<MangaSourceManager>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ import ani.dantotsu.logError
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
val data: Uri? = intent?.data
|
||||
logger(data.toString())
|
||||
try {
|
||||
|
||||
@@ -7,11 +7,13 @@ import androidx.core.os.bundleOf
|
||||
import ani.dantotsu.loadMedia
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
|
||||
class UrlMedia : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||
var isMAL = false
|
||||
var continueMedia = true
|
||||
|
||||
@@ -12,13 +12,15 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = getProcessName()
|
||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||
|
||||
@@ -8,13 +8,15 @@ import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.mal.MAL.clientId
|
||||
import ani.dantotsu.connections.mal.MAL.saveResponse
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
try {
|
||||
val data: Uri = intent?.data
|
||||
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
|
||||
class DownloadContainerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
setContentView(R.layout.activity_container)
|
||||
|
||||
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
val fragment = Class.forName(fragmentClassName).newInstance() as Fragment
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
115
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
115
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
@@ -0,0 +1,115 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
|
||||
class DownloadsManager(private val context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
|
||||
private val gson = Gson()
|
||||
private val downloadsList = loadDownloads().toMutableList()
|
||||
|
||||
val mangaDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.MANGA }
|
||||
val animeDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.ANIME }
|
||||
|
||||
private fun saveDownloads() {
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
prefs.edit().putString("downloads_key", jsonString).apply()
|
||||
}
|
||||
|
||||
private fun loadDownloads(): List<Download> {
|
||||
val jsonString = prefs.getString("downloads_key", null)
|
||||
return if (jsonString != null) {
|
||||
val type = object : TypeToken<List<Download>>() {}.type
|
||||
gson.fromJson(jsonString, type)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addDownload(download: Download) {
|
||||
downloadsList.add(download)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(download: Download) {
|
||||
downloadsList.remove(download)
|
||||
removeDirectory(download)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
private fun removeDirectory(download: Download) {
|
||||
val directory = if (download.type == Download.Type.MANGA){
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
|
||||
}
|
||||
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
|
||||
val directory = if (download.type == Download.Type.MANGA){
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
|
||||
}
|
||||
val destination = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/${download.title}/${download.chapter}")
|
||||
if (directory.exists()) {
|
||||
val copied = directory.copyRecursively(destination, true)
|
||||
if (copied) {
|
||||
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: Download.Type){
|
||||
val directory = if (type == Download.Type.MANGA){
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
downloadsList.removeAll { it.type == type }
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class MangaDownloaderService : Service() {
|
||||
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
private lateinit var builder: NotificationCompat.Builder
|
||||
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
private val downloadJobs = mutableMapOf<String, Job>()
|
||||
private val mutex = Mutex()
|
||||
private var isCurrentlyProcessing = false
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Manga Download Progress")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
startForeground(NOTIFICATION_ID, builder.build())
|
||||
ContextCompat.registerReceiver(this, cancelReceiver, IntentFilter(ACTION_CANCEL_DOWNLOAD), ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
ServiceDataSingleton.downloadQueue.clear()
|
||||
downloadJobs.clear()
|
||||
ServiceDataSingleton.isServiceRunning = false
|
||||
unregisterReceiver(cancelReceiver)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
snackString("Download started")
|
||||
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
serviceScope.launch {
|
||||
mutex.withLock {
|
||||
if (!isCurrentlyProcessing) {
|
||||
isCurrentlyProcessing = true
|
||||
processQueue()
|
||||
isCurrentlyProcessing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun processQueue() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
while (ServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||
val task = ServiceDataSingleton.downloadQueue.poll()
|
||||
if (task != null) {
|
||||
val job = launch { download(task) }
|
||||
mutex.withLock {
|
||||
downloadJobs[task.chapter] = job
|
||||
}
|
||||
job.join() // Wait for the job to complete before continuing to the next task
|
||||
mutex.withLock {
|
||||
downloadJobs.remove(task.chapter)
|
||||
}
|
||||
updateNotification() // Update the notification after each task is completed
|
||||
}
|
||||
if (ServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
stopSelf() // Stop the service when the queue is empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelDownload(chapter: String) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
mutex.withLock {
|
||||
downloadJobs[chapter]?.cancel()
|
||||
downloadJobs.remove(chapter)
|
||||
ServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
|
||||
updateNotification() // Update the notification after cancellation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
// Update the notification to reflect the current state of the queue
|
||||
val pendingDownloads = ServiceDataSingleton.downloadQueue.size
|
||||
val text = if (pendingDownloads > 0) {
|
||||
"Pending downloads: $pendingDownloads"
|
||||
} else {
|
||||
"All downloads completed"
|
||||
}
|
||||
builder.setContentText(text)
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
suspend fun download(task: DownloadTask) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
this@MangaDownloaderService,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
// Limit the number of simultaneous downloads from the task
|
||||
if (deferredList.size >= task.simultaneousDownloads) {
|
||||
// Wait for all deferred to complete and clear the list
|
||||
deferredList.awaitAll()
|
||||
deferredList.clear()
|
||||
}
|
||||
|
||||
// Download the image and add to deferred list
|
||||
val deferred = async(Dispatchers.IO) {
|
||||
var bitmap: Bitmap? = null
|
||||
var retryCount = 0
|
||||
|
||||
while (bitmap == null && retryCount < task.retries) {
|
||||
bitmap = image.fetchAndProcessImage(
|
||||
image.page,
|
||||
image.source,
|
||||
this@MangaDownloaderService
|
||||
)
|
||||
retryCount++
|
||||
}
|
||||
|
||||
// Cache the image if successful
|
||||
if (bitmap != null) {
|
||||
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
||||
}
|
||||
farthest++
|
||||
builder.setProgress(task.imageData.size, farthest, false)
|
||||
broadcastDownloadProgress(task.chapter, farthest * 100 / task.imageData.size)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
bitmap
|
||||
}
|
||||
|
||||
deferredList.add(deferred)
|
||||
}
|
||||
|
||||
// Wait for any remaining deferred to complete
|
||||
deferredList.awaitAll()
|
||||
|
||||
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||
.setProgress(0, 0, false)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
|
||||
saveMediaInfo(task)
|
||||
downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA))
|
||||
broadcastDownloadFinished(task.chapter)
|
||||
snackString("${task.title} - ${task.chapter} Download finished")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||
try {
|
||||
// Define the directory within the private external storage space
|
||||
val directory = File(
|
||||
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$title/$chapter"
|
||||
)
|
||||
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Create a file reference within that directory for your image
|
||||
val file = File(directory, fileName)
|
||||
|
||||
// Use a FileOutputStream to write the bitmap to the file
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Exception while saving image: ${e.message}")
|
||||
snackString("Exception while saving image: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${task.title}"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val mediaJson = gson.toJson(task.sourceMedia)
|
||||
val media = gson.fromJson(mediaJson, Media::class.java)
|
||||
if (media != null) {
|
||||
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
|
||||
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? = withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
try {
|
||||
connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@MangaDownloaderService, "Exception while saving ${name}: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
null
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastDownloadStarted(chapterNumber: String) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFinished(chapterNumber: String) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_FINISHED).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFailed(chapterNumber: String) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_FAILED).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadProgress(chapterNumber: String, progress: Int) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_PROGRESS).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
putExtra("progress", progress)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private val cancelReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
|
||||
val chapter = intent.getStringExtra(EXTRA_CHAPTER)
|
||||
chapter?.let {
|
||||
cancelDownload(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class DownloadTask(
|
||||
val title: String,
|
||||
val chapter: String,
|
||||
val imageData: List<ImageData>,
|
||||
val sourceMedia: Media? = null,
|
||||
val retries: Int = 2,
|
||||
val simultaneousDownloads: Int = 2,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1103
|
||||
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
|
||||
const val EXTRA_CHAPTER = "extra_chapter"
|
||||
}
|
||||
}
|
||||
|
||||
object ServiceDataSingleton {
|
||||
var imageData: List<ImageData> = listOf()
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||
@Volatile
|
||||
var isServiceRunning: Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import ani.dantotsu.R
|
||||
|
||||
|
||||
class OfflineMangaAdapter(private val context: Context, private val items: List<OfflineMangaModel>) : BaseAdapter() {
|
||||
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
override fun getCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any {
|
||||
return items[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
var view = convertView
|
||||
if (view == null) {
|
||||
view = inflater.inflate(R.layout.item_media_compact, parent, false)
|
||||
}
|
||||
|
||||
val item = getItem(position) as OfflineMangaModel
|
||||
val imageView = view!!.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||
// Bind item data to the views
|
||||
// For example:
|
||||
imageView.setImageURI(item.image)
|
||||
titleTextView.text = item.title
|
||||
itemScore.text = item.score
|
||||
if (item.isOngoing) {
|
||||
ongoing.visibility = View.VISIBLE
|
||||
} else {
|
||||
ongoing.visibility = View.GONE
|
||||
}
|
||||
return view
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.GridView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class OfflineMangaFragment: Fragment() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
private var downloads: List<OfflineMangaModel> = listOf()
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineMangaAdapter
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
|
||||
|
||||
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
val animeUserAvatar= view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
|
||||
animeUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
getDownloads()
|
||||
adapter = OfflineMangaAdapter(requireContext(), downloads)
|
||||
gridView.adapter = adapter
|
||||
gridView.setOnItemClickListener { parent, view, position, id ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val media = downloadManager.mangaDownloads.filter { it.title == item.title }.first()
|
||||
startActivity(
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(media))
|
||||
.putExtra("download", true)
|
||||
)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
var height = statusBarHeight
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
height = max(
|
||||
statusBarHeight,
|
||||
min(
|
||||
displayCutout.boundingRects[0].width(),
|
||||
displayCutout.boundingRects[0].height()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||
var visible = false
|
||||
fun animate() {
|
||||
val start = if (visible) 0f else 1f
|
||||
val end = if (!visible) 0f else 1f
|
||||
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
|
||||
duration = 300
|
||||
interpolator = OvershootInterpolator(2f)
|
||||
start()
|
||||
}
|
||||
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
|
||||
duration = 300
|
||||
interpolator = OvershootInterpolator(2f)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop.setOnClickListener {
|
||||
//TODO: scroll to top
|
||||
}
|
||||
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
downloads = listOf()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
downloads = listOf()
|
||||
}
|
||||
private fun getDownloads() {
|
||||
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
|
||||
val newDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in titles) {
|
||||
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newDownloads
|
||||
}
|
||||
|
||||
private fun getMedia(download: Download): Media? {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${download.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
return gson.fromJson(mediaJson, Media::class.java)
|
||||
}
|
||||
catch (e: Exception){
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel{
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${download.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val mediaModel = getMedia(download)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val title = mediaModel.nameMAL?:"unknown"
|
||||
val score = if (mediaModel.userScore != 0) mediaModel.userScore.toString() else
|
||||
if (mediaModel.meanScore == null) "?" else mediaModel.meanScore.toString()
|
||||
val isOngoing = false
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
|
||||
}
|
||||
catch (e: Exception){
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
return OfflineMangaModel("unknown", "0", false, false, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
data class OfflineMangaModel(val title: String, val score: String, val isOngoing: Boolean, val isUserScored: Boolean, val image: Uri?) {
|
||||
}
|
||||
@@ -28,6 +28,9 @@ import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.*
|
||||
@@ -118,6 +121,9 @@ object Helper {
|
||||
val database = StandaloneDatabaseProvider(context)
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val okHttpClient = networkHelper.client
|
||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
|
||||
@@ -24,7 +24,7 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
|
||||
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
|
||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||
this,
|
||||
R.drawable.monochrome,
|
||||
R.drawable.mono,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -19,6 +22,7 @@ import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.MediaPageTransformer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
@@ -54,10 +58,20 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
|
||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
@@ -81,7 +95,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
binding.animeUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.ANIME).show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
listOf(
|
||||
|
||||
@@ -107,7 +107,7 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show(parentFragmentManager, "dialog")
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show(parentFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -19,6 +21,7 @@ import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.MediaPageTransformer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
@@ -53,10 +56,20 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0x80000000.toInt()
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
|
||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
@@ -76,7 +89,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
|
||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
||||
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.MANGA).show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
binding.mangaSearchBar.setEndIconOnClickListener {
|
||||
|
||||
@@ -1,32 +1,111 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.ActivityNoInternetBinding
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.offline.OfflineFragment
|
||||
import ani.dantotsu.selectedOption
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
|
||||
class NoInternet : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityNoInternetBinding
|
||||
lateinit var bottomBar: AnimatedBottomBar
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
val binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||
binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
}
|
||||
binding.refreshButton.setOnClickListener {
|
||||
if (isOnline(this)) {
|
||||
startMainActivity(this)
|
||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
}
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
ani.dantotsu.bottomBar = navbar
|
||||
navbar.visibility = View.VISIBLE
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
navbar.animate().translationZ(12f).setDuration(200).start()
|
||||
selectedOption = newIndex
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
|
||||
//supportFragmentManager.beginTransaction().replace(binding.fragmentContainer.id, OfflineFragment()).commit()
|
||||
|
||||
}
|
||||
|
||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
when (position) {
|
||||
0 -> return OfflineFragment()
|
||||
1 -> return OfflineFragment()
|
||||
2 -> return OfflineMangaFragment()
|
||||
}
|
||||
return LoginFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.ActivityAuthorBinding
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -29,7 +30,8 @@ class AuthorActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -33,26 +34,30 @@ class CalendarActivity : AppCompatActivity() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
|
||||
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, typedValue, true)
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
val primaryColor = typedValue.data
|
||||
val typedValue2 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true)
|
||||
val primaryTextColor = typedValue2.data
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue2, true)
|
||||
val titleTextColor = typedValue2.data
|
||||
val typedValue3 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue3, true)
|
||||
val secondaryColor = typedValue3.data
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||
val primaryTextColor = typedValue3.data
|
||||
val typedValue4 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
|
||||
val secondaryTextColor = typedValue4.data
|
||||
|
||||
window.statusBarColor = primaryColor
|
||||
window.navigationBarColor = primaryColor
|
||||
binding.listTabLayout.setBackgroundColor(primaryColor)
|
||||
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
binding.listTabLayout.setTabTextColors(primaryTextColor, primaryTextColor)
|
||||
binding.listTitle.setTextColor(titleTextColor)
|
||||
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) {
|
||||
@@ -103,4 +108,4 @@ class CalendarActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -34,7 +35,8 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -26,7 +27,8 @@ class GenreActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityGenreBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
initActivity(this)
|
||||
|
||||
@@ -22,7 +22,7 @@ data class Media(
|
||||
val userPreferredName: String,
|
||||
|
||||
var cover: String? = null,
|
||||
val banner: String? = null,
|
||||
var banner: String? = null,
|
||||
var relation: String? = null,
|
||||
var popularity: Int? = null,
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
@@ -72,7 +73,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
@@ -118,7 +120,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
|
||||
var media: Media = intent.getSerialized("media") ?: return
|
||||
media.selected = model.loadSelected(media)
|
||||
val isDownload = intent.getBooleanExtra("download", false)
|
||||
media.selected = model.loadSelected(media, isDownload)
|
||||
|
||||
binding.mediaCoverImage.loadImage(media.cover)
|
||||
binding.mediaCoverImage.setOnLongClickListener {
|
||||
@@ -324,7 +327,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
tabLayout.setOnItemSelectedListener { item ->
|
||||
selectFromID(item.itemId)
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
val sel = model.loadSelected(media)
|
||||
val sel = model.loadSelected(media, isDownload)
|
||||
sel.window = selected
|
||||
model.saveSelected(media.id, sel, this)
|
||||
true
|
||||
|
||||
@@ -3,12 +3,14 @@ package ani.dantotsu.media
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
@@ -30,6 +32,8 @@ import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.AniyomiAdapter
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
@@ -44,6 +48,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class MediaDetailsViewModel : ViewModel() {
|
||||
val scrolledToTop = MutableLiveData(true)
|
||||
@@ -53,17 +58,23 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
|
||||
fun loadSelected(media: Media): Selected {
|
||||
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
|
||||
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)
|
||||
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
|
||||
}
|
||||
if (isDownload) {
|
||||
data.sourceIndex = when (media.anime != null) {
|
||||
true -> AnimeSources.list.size - 1
|
||||
else -> MangaSources.list.size - 1
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -120,8 +131,8 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
|
||||
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
|
||||
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
|
||||
suspend fun loadEpisodes(media: Media, i: Int) {
|
||||
if (!epsLoaded.containsKey(i)) {
|
||||
suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) {
|
||||
if (!epsLoaded.containsKey(i) || invalidate) {
|
||||
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
|
||||
}
|
||||
episodes.postValue(epsLoaded)
|
||||
@@ -240,9 +251,9 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
|
||||
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
|
||||
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters
|
||||
suspend fun loadMangaChapters(media: Media, i: Int) {
|
||||
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
||||
logger("Loading Manga Chapters : $mangaLoaded")
|
||||
if (!mangaLoaded.containsKey(i)) tryWithSuspend {
|
||||
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
||||
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||
}
|
||||
mangaChapters.postValue(mangaLoaded)
|
||||
@@ -258,7 +269,28 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
|
||||
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
||||
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
||||
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
|
||||
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, series: String, post: Boolean = true): Boolean {
|
||||
//check if the chapter has been downloaded already
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
if(downloadsManager.mangaDownloads.contains(Download(series, chapter.title!!, Download.Type.MANGA))) {
|
||||
val download = downloadsManager.mangaDownloads.find { it.title == series && it.chapter == chapter.title!! } ?: return false
|
||||
//look in the downloads folder for the chapter and add all the numerically named images to the chapter
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$series/${chapter.title!!}"
|
||||
)
|
||||
val images = mutableListOf<MangaImage>()
|
||||
directory.listFiles()?.forEach {
|
||||
if (it.nameWithoutExtension.toIntOrNull() != null) {
|
||||
images.add(MangaImage(FileUrl(it.absolutePath), false))
|
||||
}
|
||||
}
|
||||
//sort the images by name
|
||||
images.sortBy { it.url.url }
|
||||
chapter.addImages(images)
|
||||
if (post) mangaChapter.postValue(chapter)
|
||||
return true
|
||||
}
|
||||
return tryWithSuspend(true) {
|
||||
chapter.addImages(
|
||||
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
|
||||
|
||||
@@ -16,6 +16,7 @@ import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -17,6 +17,7 @@ import ani.dantotsu.connections.anilist.AnilistSearch
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
@@ -38,7 +39,8 @@ class SearchActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivitySearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
initActivity(this)
|
||||
|
||||
@@ -9,8 +9,10 @@ data class Selected(
|
||||
var chip: Int = 0,
|
||||
//var source: String = "",
|
||||
var sourceIndex: Int = 0,
|
||||
var langIndex: Int = 0,
|
||||
var preferDub: Boolean = false,
|
||||
var server: String? = null,
|
||||
var video: Int = 0,
|
||||
var latest: Float = 0f,
|
||||
var scanlators: List<String>? = null,
|
||||
) : Serializable
|
||||
|
||||
@@ -16,6 +16,7 @@ import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.ActivityStudioBinding
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -29,7 +30,8 @@ class StudioActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityStudioBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.TypedValue
|
||||
@@ -8,22 +10,38 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.settings.ExtensionsActivity
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.IndexOutOfBoundsException
|
||||
|
||||
class AnimeWatchAdapter(
|
||||
private val media: Media,
|
||||
@@ -69,7 +87,8 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
|
||||
//Source Selection
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||
var source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||
setLanguageList(media.selected!!.langIndex,source)
|
||||
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||
binding.animeSource.setText(watchSources.names[source])
|
||||
watchSources[source].apply {
|
||||
@@ -91,11 +110,41 @@ class AnimeWatchAdapter(
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||
source = i
|
||||
setLanguageList(0,i)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadEpisodes(i)
|
||||
fragment.loadEpisodes(i, false)
|
||||
}
|
||||
|
||||
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
ext.sourceLanguage = i
|
||||
fragment.onLangChange(i)
|
||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||
setLanguageList(i, source)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
} ?: run {
|
||||
}
|
||||
}
|
||||
|
||||
//settings
|
||||
binding.animeSourceSettings.setOnClickListener {
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
fragment.openSettings(ext.extension)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Subscription
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
@@ -177,6 +226,7 @@ class AnimeWatchAdapter(
|
||||
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
||||
}
|
||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
|
||||
|
||||
chip.setOnClickListener {
|
||||
selected()
|
||||
@@ -261,6 +311,25 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (watchSources is AnimeSources) {
|
||||
val parser = watchSources[source] as? DynamicAnimeParser
|
||||
if (parser != null) {
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
ext.sourceLanguage = lang
|
||||
}
|
||||
try {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
}catch (e: IndexOutOfBoundsException) {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown")
|
||||
}
|
||||
binding?.animeSourceLanguage?.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, parser.extension.sources.map { it.lang }))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -13,24 +19,38 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.parsers.AnimeParser
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.settings.ExtensionsActivity
|
||||
import ani.dantotsu.settings.InstalledAnimeExtensionsFragment
|
||||
import ani.dantotsu.settings.PlayerSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.subcriptions.Notifications
|
||||
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
@@ -214,6 +234,13 @@ class AnimeWatchFragment : Fragment() {
|
||||
return model.watchSources?.get(i)!!
|
||||
}
|
||||
|
||||
fun onLangChange(i: Int) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.langIndex = i
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
fun onDubClicked(checked: Boolean) {
|
||||
val selected = model.loadSelected(media)
|
||||
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
||||
@@ -223,8 +250,8 @@ class AnimeWatchFragment : Fragment() {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
|
||||
}
|
||||
|
||||
fun loadEpisodes(i: Int) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) }
|
||||
fun loadEpisodes(i: Int, invalidate: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
|
||||
}
|
||||
|
||||
fun onIconPressed(viewType: Int, rev: Boolean) {
|
||||
@@ -262,45 +289,115 @@ class AnimeWatchFragment : Fragment() {
|
||||
else getString(R.string.unsubscribed_notification)
|
||||
)
|
||||
}
|
||||
|
||||
fun onEpisodeClick(i: String) {
|
||||
model.continueMedia = false
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
|
||||
//Find latest episode for subscription
|
||||
selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
||||
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
headerAdapter.handleEpisodes()
|
||||
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
|
||||
var arr: ArrayList<Episode> = arrayListOf()
|
||||
if (media.anime!!.episodes != null) {
|
||||
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
|
||||
arr.addAll(
|
||||
media.anime!!.episodes!!.values.toList()
|
||||
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
|
||||
)
|
||||
if (reverse)
|
||||
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
|
||||
fun openSettings(pkg: AnimeExtension.Installed){
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as MediaDetailsActivity
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||
try{
|
||||
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||
}catch (e: ClassCastException){
|
||||
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||
}
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
val fragment =
|
||||
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||
changeUIVisibility(true)
|
||||
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
changeUIVisibility(true)
|
||||
return@setNegativeButton
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||
changeUIVisibility(true)
|
||||
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
changeUIVisibility(false)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
episodeAdapter.arr = arr
|
||||
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
|
||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
model.watchSources?.flushText()
|
||||
super.onDestroy()
|
||||
}
|
||||
fun onEpisodeClick(i: String) {
|
||||
model.continueMedia = false
|
||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
|
||||
}
|
||||
|
||||
var state: Parcelable? = null
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
|
||||
//Find latest episode for subscription
|
||||
selected.latest =
|
||||
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
||||
selected.latest =
|
||||
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
headerAdapter.handleEpisodes()
|
||||
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
|
||||
var arr: ArrayList<Episode> = arrayListOf()
|
||||
if (media.anime!!.episodes != null) {
|
||||
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
|
||||
arr.addAll(
|
||||
media.anime!!.episodes!!.values.toList()
|
||||
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
|
||||
)
|
||||
if (reverse)
|
||||
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
|
||||
}
|
||||
episodeAdapter.arr = arr
|
||||
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
|
||||
episodeAdapter.notifyItemRangeInserted(0, arr.size)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
model.watchSources?.flushText()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
var state: Parcelable? = null
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.mediaInfoProgressBar.visibility = progress
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.graphics.drawable.Animatable
|
||||
import android.hardware.SensorManager
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.*
|
||||
import android.media.PlaybackParams
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -77,8 +78,10 @@ import ani.dantotsu.settings.PlayerSettings
|
||||
import ani.dantotsu.settings.PlayerSettingsActivity
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.lagradost.nicehttp.ignoreAllSSLErrors
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -317,7 +320,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityExoplayerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -806,23 +810,24 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
|
||||
fun fastForward() {
|
||||
val newSpeed = playbackParameters.speed * 2f
|
||||
exoPlayer.playbackParameters = playbackParameters.withSpeed(newSpeed)
|
||||
isFastForwarding = true
|
||||
snackString("Playing at ${newSpeed}x speed")
|
||||
exoPlayer.setPlaybackSpeed(2f)
|
||||
snackString("Playing at 2x speed")
|
||||
}
|
||||
|
||||
fun stopFastForward() {
|
||||
if (isFastForwarding) {
|
||||
exoPlayer.playbackParameters = playbackParameters
|
||||
isFastForwarding = false
|
||||
exoPlayer.setPlaybackSpeed(1f)
|
||||
snackString("Playing at normal speed")
|
||||
}
|
||||
}
|
||||
|
||||
//FastRewind (Left Panel)
|
||||
val fastRewindDetector = GestureDetector(this, object : GesturesListener() {
|
||||
override fun onLongClick(event: MotionEvent) = fastForward()
|
||||
override fun onLongClick(event: MotionEvent) {
|
||||
if (settings.fastforward) fastForward()
|
||||
}
|
||||
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
doubleTap(false, event)
|
||||
@@ -852,8 +857,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
|
||||
//FastForward (Right Panel)
|
||||
val fastForwardDetector = GestureDetector(this, object : GesturesListener() {
|
||||
override fun onLongClick(event: MotionEvent) = fastForward()
|
||||
|
||||
override fun onLongClick(event: MotionEvent) {
|
||||
if (settings.fastforward) fastForward()
|
||||
}
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
doubleTap(true, event)
|
||||
}
|
||||
@@ -1614,7 +1620,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||
}
|
||||
|
||||
else
|
||||
-> toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
|
||||
-> {
|
||||
toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ package ani.dantotsu.media.anime
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -24,6 +26,7 @@ import ani.dantotsu.others.Download.download
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -53,6 +56,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
||||
val window = dialog?.window
|
||||
window?.statusBarColor = Color.TRANSPARENT
|
||||
val typedValue = TypedValue()
|
||||
val theme = requireContext().theme
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
window?.navigationBarColor = typedValue.data
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -236,7 +245,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
if (video.format == VideoType.CONTAINER) {
|
||||
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
|
||||
binding.urlSize.text =
|
||||
(if (video.extraNote != null) " : " else "") + DecimalFormat("#.##").format(video.size ?: 0).toString() + " MB"
|
||||
// if video size is null or 0, show "Unknown Size" else show the size in MB
|
||||
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat("#.##").format(video.size ?: 0).toString()+ " MB"))
|
||||
}
|
||||
else {
|
||||
binding.urlQuality.text = "Multi Quality"
|
||||
|
||||
@@ -10,6 +10,9 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.LruCache
|
||||
import android.widget.Toast
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.snackString
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -26,22 +29,23 @@ data class ImageData(
|
||||
try {
|
||||
// Fetch the image
|
||||
val response = httpSource.getImage(page)
|
||||
println("Response: ${response.code}")
|
||||
println("Response: ${response.message}")
|
||||
logger("Response: ${response.code}")
|
||||
logger("Response: ${response.message}")
|
||||
|
||||
// Convert the Response to an InputStream
|
||||
val inputStream = response.body?.byteStream()
|
||||
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)
|
||||
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}")
|
||||
logger("An error occurred: ${e.message}")
|
||||
snackString("An error occurred: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
@@ -57,7 +61,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga")
|
||||
}
|
||||
|
||||
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
val uri: Uri? = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { os ->
|
||||
@@ -65,7 +69,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ data class MangaChapter(
|
||||
var link: String,
|
||||
var title: String? = null,
|
||||
var description: String? = null,
|
||||
var sChapter: SChapter
|
||||
var sChapter: SChapter,
|
||||
val scanlator: String? = null,
|
||||
var progress: String? = ""
|
||||
) : Serializable {
|
||||
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)
|
||||
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter, chapter.scanlator)
|
||||
|
||||
private val images = mutableListOf<MangaImage>()
|
||||
fun images(): List<MangaImage> = images
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemChapterListBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.media.Media
|
||||
@@ -28,7 +29,15 @@ class MangaChapterAdapter(
|
||||
false
|
||||
)
|
||||
)
|
||||
0 -> ChapterListViewHolder(ItemChapterListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
|
||||
0 -> ChapterListViewHolder(
|
||||
ItemChapterListBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
@@ -39,7 +48,8 @@ class MangaChapterAdapter(
|
||||
|
||||
override fun getItemCount(): Int = arr.size
|
||||
|
||||
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||
@@ -48,12 +58,102 @@ class MangaChapterAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
private val activeDownloads = mutableSetOf<String>()
|
||||
private val downloadedChapters = mutableSetOf<String>()
|
||||
|
||||
fun startDownload(chapterNumber: String) {
|
||||
activeDownloads.add(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
if (position != -1) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopDownload(chapterNumber: String) {
|
||||
activeDownloads.remove(chapterNumber)
|
||||
downloadedChapters.add(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
if (position != -1) {
|
||||
arr[position].progress = "Downloaded"
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDownload(chapterNumber: String) {
|
||||
downloadedChapters.remove(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
if (position != -1) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeDownload(chapterNumber: String) {
|
||||
activeDownloads.remove(chapterNumber)
|
||||
downloadedChapters.remove(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
if (position != -1) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDownloadProgress(chapterNumber: String, progress: Int) {
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
if (position != -1) {
|
||||
arr[position].progress = "Downloading: ${progress}%"
|
||||
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(chapterNumber: String, progress: String?) {
|
||||
if (progress != null) {
|
||||
binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
binding.itemChapterTitle.text = "$progress"
|
||||
}else{
|
||||
binding.itemChapterTitle.visibility = View.GONE
|
||||
binding.itemChapterTitle.text = ""
|
||||
}
|
||||
if (activeDownloads.contains(chapterNumber)) {
|
||||
// Show spinner
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_round_refresh_24)
|
||||
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||
// Show checkmark
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_check)
|
||||
} else {
|
||||
// Show download icon
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_round_download_24)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
||||
}
|
||||
binding.itemDownload.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
|
||||
val chapterNumber = arr[bindingAdapterPosition].number
|
||||
if (activeDownloads.contains(chapterNumber)) {
|
||||
fragment.onMangaChapterStopDownloadClick(chapterNumber)
|
||||
return@setOnClickListener
|
||||
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||
startDownload(chapterNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,44 +163,50 @@ class MangaChapterAdapter(
|
||||
val binding = holder.binding
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
val ep = arr[position]
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
|
||||
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
|
||||
if (media.userProgress != null) {
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 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, MangaNameAdapter.findChapterNumber(ep.number).toString())
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(ep.number).toString()
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChapterListViewHolder -> {
|
||||
|
||||
is ChapterListViewHolder -> {
|
||||
val binding = holder.binding
|
||||
val ep = arr[position]
|
||||
holder.bind(ep.number, ep.progress)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
binding.itemChapterNumber.text = ep.number
|
||||
if (!ep.title.isNullOrEmpty()) {
|
||||
binding.itemChapterTitle.text = ep.title
|
||||
binding.itemChapterTitle.setOnLongClickListener {
|
||||
binding.itemChapterTitle.maxLines.apply {
|
||||
binding.itemChapterTitle.maxLines = if (this == 1) 3 else 1
|
||||
}
|
||||
true
|
||||
}
|
||||
binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
} else binding.itemChapterTitle.visibility = View.GONE
|
||||
if (ep.progress.isNullOrEmpty()) {
|
||||
binding.itemChapterTitle.visibility = View.GONE
|
||||
} else binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
|
||||
if (media.userProgress != null) {
|
||||
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 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, MangaNameAdapter.findChapterNumber(ep.number).toString())
|
||||
updateProgress(
|
||||
media,
|
||||
MangaNameAdapter.findChapterNumber(ep.number).toString()
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -117,4 +223,4 @@ class MangaChapterAdapter(
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,36 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.App.Companion.context
|
||||
import ani.dantotsu.media.anime.handleProgress
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.MangaReadSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.IndexOutOfBoundsException
|
||||
|
||||
class MangaReadAdapter(
|
||||
private val media: Media,
|
||||
@@ -31,6 +40,9 @@ class MangaReadAdapter(
|
||||
|
||||
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
||||
private var _binding: ItemAnimeWatchBinding? = null
|
||||
val hiddenScanlators = mutableListOf<String>()
|
||||
var scanlatorSelectionListener: ScanlatorSelectionListener? = null
|
||||
var options = listOf<String>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
@@ -49,10 +61,10 @@ class MangaReadAdapter(
|
||||
}
|
||||
|
||||
//Source Selection
|
||||
val source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
var source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
setLanguageList(media.selected!!.langIndex,source)
|
||||
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||
binding.animeSource.setText(mangaReadSources.names[source])
|
||||
|
||||
mangaReadSources[source].apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
@@ -64,9 +76,36 @@ class MangaReadAdapter(
|
||||
fragment.onSourceChange(i).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
source = i
|
||||
setLanguageList(0,i)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadChapters(i)
|
||||
//invalidate if it's the last source
|
||||
val invalidate = i == mangaReadSources.names.size - 1
|
||||
fragment.loadChapters(i, invalidate)
|
||||
}
|
||||
|
||||
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
ext.sourceLanguage = i
|
||||
fragment.onLangChange(i)
|
||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
setLanguageList(i, source)
|
||||
}
|
||||
subscribeButton(false)
|
||||
fragment.loadChapters(media.selected!!.sourceIndex, true)
|
||||
} ?: run {
|
||||
}
|
||||
}
|
||||
|
||||
//settings
|
||||
binding.animeSourceSettings.setOnClickListener {
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
fragment.openSettings(ext.extension)
|
||||
}
|
||||
}
|
||||
|
||||
//Subscription
|
||||
@@ -98,6 +137,46 @@ class MangaReadAdapter(
|
||||
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
fragment.onIconPressed(style, reversed)
|
||||
}
|
||||
|
||||
binding.animeScanlatorTop.setOnClickListener {
|
||||
val dialogView = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||
|
||||
// Dynamically add checkboxes
|
||||
|
||||
options.forEach { option ->
|
||||
val checkBox = CheckBox(currContext()).apply {
|
||||
text = option
|
||||
}
|
||||
//set checked if it's already selected
|
||||
if(media.selected!!.scanlators != null){
|
||||
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
}else{
|
||||
checkBox.isChecked = true
|
||||
}
|
||||
checkboxContainer.addView(checkBox)
|
||||
}
|
||||
|
||||
// Create AlertDialog
|
||||
AlertDialog.Builder(currContext())
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { dialog, which ->
|
||||
//add unchecked to hidden
|
||||
hiddenScanlators.clear()
|
||||
for (i in 0 until checkboxContainer.childCount) {
|
||||
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||
if (!checkBox.isChecked) {
|
||||
hiddenScanlators.add(checkBox.text.toString())
|
||||
}
|
||||
}
|
||||
media.selected!!.scanlators = hiddenScanlators
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
var selected = when (style) {
|
||||
0 -> binding.animeSourceList
|
||||
1 -> binding.animeSourceCompact
|
||||
@@ -145,6 +224,7 @@ class MangaReadAdapter(
|
||||
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
||||
}
|
||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
|
||||
|
||||
chip.setOnClickListener {
|
||||
selected()
|
||||
@@ -167,6 +247,7 @@ class MangaReadAdapter(
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun handleChapters() {
|
||||
|
||||
val binding = _binding
|
||||
if (binding != null) {
|
||||
if (media.manga?.chapters != null) {
|
||||
@@ -174,7 +255,11 @@ class MangaReadAdapter(
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
val formattedChapters = chapters.map { MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() }
|
||||
val filteredChapters = chapters.filter { chapterKey ->
|
||||
val chapter = media.manga.chapters!![chapterKey]!!
|
||||
chapter.scanlator !in hiddenScanlators
|
||||
}
|
||||
val formattedChapters = filteredChapters.map { MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() }
|
||||
if (formattedChapters.contains(continueEp)) {
|
||||
continueEp = chapters[formattedChapters.indexOf(continueEp)]
|
||||
binding.animeSourceContinue.visibility = View.VISIBLE
|
||||
@@ -222,7 +307,30 @@ class MangaReadAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguageList(lang: Int, source: Int) {
|
||||
val binding = _binding
|
||||
if (mangaReadSources is MangaSources) {
|
||||
val parser = mangaReadSources[source] as? DynamicMangaParser
|
||||
if (parser != null) {
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
ext.sourceLanguage = lang
|
||||
}
|
||||
try {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
}catch (e: IndexOutOfBoundsException) {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown")
|
||||
}
|
||||
binding?.animeSourceLanguage?.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, parser.extension.sources.map { it.lang }))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
}
|
||||
|
||||
interface ScanlatorSelectionListener {
|
||||
fun onScanlatorsSelected()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -13,27 +26,48 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.ServiceDataSingleton
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaParser
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import ani.dantotsu.subcriptions.Notifications
|
||||
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import android.Manifest
|
||||
import androidx.core.app.ActivityCompat
|
||||
|
||||
open class MangaReadFragment : Fragment() {
|
||||
open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
private var _binding: FragmentAnimeWatchBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val model: MediaDetailsViewModel by activityViewModels()
|
||||
@@ -48,6 +82,8 @@ open class MangaReadFragment : Fragment() {
|
||||
private lateinit var headerAdapter: MangaReadAdapter
|
||||
private lateinit var chapterAdapter: MangaChapterAdapter
|
||||
|
||||
val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
var screenWidth = 0f
|
||||
private var progress = View.VISIBLE
|
||||
|
||||
@@ -67,10 +103,18 @@ open class MangaReadFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(ACTION_DOWNLOAD_STARTED)
|
||||
addAction(ACTION_DOWNLOAD_FINISHED)
|
||||
addAction(ACTION_DOWNLOAD_FAILED)
|
||||
addAction(ACTION_DOWNLOAD_PROGRESS)
|
||||
}
|
||||
|
||||
ContextCompat.registerReceiver(requireContext(), downloadStatusReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
|
||||
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||
|
||||
|
||||
var maxGridSize = (screenWidth / 100f).roundToInt()
|
||||
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
|
||||
|
||||
@@ -116,8 +160,13 @@ open class MangaReadFragment : Fragment() {
|
||||
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
|
||||
|
||||
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
|
||||
headerAdapter.scanlatorSelectionListener = this
|
||||
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
||||
|
||||
for (download in downloadManager.mangaDownloads){
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
|
||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@@ -134,45 +183,70 @@ open class MangaReadFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters ->
|
||||
if (loadedChapters != null) {
|
||||
val chapters = loadedChapters[media.selected!!.sourceIndex]
|
||||
if (chapters != null) {
|
||||
media.manga?.chapters = chapters
|
||||
model.getMangaChapters().observe(viewLifecycleOwner) { _ ->
|
||||
updateChapters()
|
||||
}
|
||||
}
|
||||
|
||||
//CHIP GROUP
|
||||
val total = chapters.size
|
||||
val divisions = total.toDouble() / 10
|
||||
start = 0
|
||||
end = null
|
||||
val limit = when {
|
||||
(divisions < 25) -> 25
|
||||
(divisions < 50) -> 50
|
||||
else -> 100
|
||||
}
|
||||
headerAdapter.clearChips()
|
||||
if (total > limit) {
|
||||
val arr = chapters.keys.toTypedArray()
|
||||
val stored = ceil((total).toDouble() / limit).toInt()
|
||||
val position = clamp(media.selected!!.chip, 0, stored - 1)
|
||||
val last = if (position + 1 == stored) total else (limit * (position + 1))
|
||||
start = limit * (position)
|
||||
end = last - 1
|
||||
headerAdapter.updateChips(
|
||||
limit,
|
||||
arr,
|
||||
(1..stored).toList().toTypedArray(),
|
||||
position
|
||||
)
|
||||
}
|
||||
override fun onScanlatorsSelected() {
|
||||
updateChapters()
|
||||
}
|
||||
|
||||
headerAdapter.subscribeButton(true)
|
||||
reload()
|
||||
private fun updateChapters() {
|
||||
val loadedChapters = model.getMangaChapters().value
|
||||
if (loadedChapters != null) {
|
||||
val chapters = loadedChapters[media.selected!!.sourceIndex]
|
||||
if (chapters != null) {
|
||||
headerAdapter.options = getScanlators(chapters)
|
||||
val filteredChapters = chapters.filterNot { (_, chapter) ->
|
||||
chapter.scanlator in headerAdapter.hiddenScanlators
|
||||
}
|
||||
|
||||
media.manga?.chapters = filteredChapters.toMutableMap()
|
||||
|
||||
//CHIP GROUP
|
||||
val total = filteredChapters.size
|
||||
val divisions = total.toDouble() / 10
|
||||
start = 0
|
||||
end = null
|
||||
val limit = when {
|
||||
(divisions < 25) -> 25
|
||||
(divisions < 50) -> 50
|
||||
else -> 100
|
||||
}
|
||||
headerAdapter.clearChips()
|
||||
if (total > limit) {
|
||||
val arr = filteredChapters.keys.toTypedArray()
|
||||
val stored = ceil((total).toDouble() / limit).toInt()
|
||||
val position = clamp(media.selected!!.chip, 0, stored - 1)
|
||||
val last = if (position + 1 == stored) total else (limit * (position + 1))
|
||||
start = limit * (position)
|
||||
end = last - 1
|
||||
headerAdapter.updateChips(
|
||||
limit,
|
||||
arr,
|
||||
(1..stored).toList().toTypedArray(),
|
||||
position
|
||||
)
|
||||
}
|
||||
|
||||
headerAdapter.subscribeButton(true)
|
||||
reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
|
||||
val scanlators = mutableListOf<String>()
|
||||
if (chap != null) {
|
||||
val chapters = chap.values
|
||||
for (chapter in chapters) {
|
||||
scanlators.add(chapter.scanlator ?: "Unknown")
|
||||
}
|
||||
}
|
||||
return scanlators.distinct()
|
||||
}
|
||||
|
||||
fun onSourceChange(i: Int): MangaParser {
|
||||
media.manga?.chapters = null
|
||||
reload()
|
||||
@@ -185,8 +259,16 @@ open class MangaReadFragment : Fragment() {
|
||||
return model.mangaReadSources?.get(i)!!
|
||||
}
|
||||
|
||||
fun loadChapters(i: Int) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) }
|
||||
fun onLangChange(i: Int) {
|
||||
val selected = model.loadSelected(media)
|
||||
selected.langIndex = i
|
||||
model.saveSelected(media.id, selected, requireActivity())
|
||||
media.selected = selected
|
||||
}
|
||||
|
||||
|
||||
fun loadChapters(i: Int, invalidate: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i, invalidate) }
|
||||
}
|
||||
|
||||
fun onIconPressed(viewType: Int, rev: Boolean) {
|
||||
@@ -225,6 +307,75 @@ open class MangaReadFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
fun openSettings(pkg: MangaExtension.Installed){
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as MediaDetailsActivity
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||
try{
|
||||
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||
}catch (e: ClassCastException){
|
||||
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||
}
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
val fragment =
|
||||
MangaSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||
changeUIVisibility(true)
|
||||
loadChapters(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
changeUIVisibility(true)
|
||||
return@setNegativeButton
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||
changeUIVisibility(true)
|
||||
loadChapters(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
changeUIVisibility(false)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onMangaChapterClick(i: String) {
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let {
|
||||
@@ -234,6 +385,112 @@ open class MangaReadFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onMangaChapterDownloadClick(i: String) {
|
||||
if (!isNotificationPermissionGranted()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let { chapter ->
|
||||
val parser = model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
|
||||
parser?.let {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val images = parser.imageList("", chapter.sChapter)
|
||||
|
||||
// Create a download task
|
||||
val downloadTask = MangaDownloaderService.DownloadTask(
|
||||
title = media.nameMAL ?: "",
|
||||
chapter = chapter.title!!,
|
||||
imageData = images,
|
||||
sourceMedia = media,
|
||||
retries = 2,
|
||||
simultaneousDownloads = 2
|
||||
)
|
||||
|
||||
ServiceDataSingleton.downloadQueue.offer(downloadTask)
|
||||
|
||||
// If the service is not already running, start it
|
||||
if (!ServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, MangaDownloaderService::class.java)
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(requireContext(), intent)
|
||||
}
|
||||
ServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
|
||||
// Inform the adapter that the download has started
|
||||
withContext(Dispatchers.Main) {
|
||||
chapterAdapter.startDownload(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotificationPermissionGranted(): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
fun onMangaChapterRemoveDownloadClick(i: String){
|
||||
downloadManager.removeDownload(Download(media.nameMAL!!, i, Download.Type.MANGA))
|
||||
chapterAdapter.deleteDownload(i)
|
||||
}
|
||||
fun onMangaChapterStopDownloadClick(i: String) {
|
||||
val cancelIntent = Intent().apply {
|
||||
action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD
|
||||
putExtra(MangaDownloaderService.EXTRA_CHAPTER, i)
|
||||
}
|
||||
requireContext().sendBroadcast(cancelIntent)
|
||||
|
||||
// Remove the download from the manager and update the UI
|
||||
downloadManager.removeDownload(Download(media.nameMAL!!, i, Download.Type.MANGA))
|
||||
chapterAdapter.stopDownload(i)
|
||||
}
|
||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_DOWNLOAD_STARTED -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||
chapterNumber?.let { chapterAdapter.startDownload(it) }
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_FINISHED -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||
chapterNumber?.let { chapterAdapter.stopDownload(it) }
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_FAILED -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||
chapterNumber?.let {
|
||||
chapterAdapter.removeDownload(it)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_PROGRESS -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||
val progress = intent.getIntExtra("progress", 0)
|
||||
chapterNumber?.let {
|
||||
chapterAdapter.updateDownloadProgress(it, progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
@@ -262,6 +519,7 @@ open class MangaReadFragment : Fragment() {
|
||||
override fun onDestroy() {
|
||||
model.mangaReadSources?.flushText()
|
||||
super.onDestroy()
|
||||
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||
}
|
||||
|
||||
private var state: Parcelable? = null
|
||||
@@ -275,4 +533,12 @@ open class MangaReadFragment : Fragment() {
|
||||
super.onPause()
|
||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
|
||||
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
|
||||
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
|
||||
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
|
||||
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.net.Uri
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
@@ -29,6 +30,7 @@ import kotlinx.coroutines.withContext
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
abstract class BaseImageAdapter(
|
||||
val activity: MangaReaderActivity,
|
||||
@@ -116,7 +118,7 @@ abstract class BaseImageAdapter(
|
||||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||
|
||||
companion object {
|
||||
suspend fun Context.loadBitmap_old(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
||||
suspend fun Context.loadBitmap_old(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? { //still used in some places
|
||||
return tryWithSuspend {
|
||||
withContext(Dispatchers.IO) {
|
||||
Glide.with(this@loadBitmap_old)
|
||||
@@ -151,8 +153,10 @@ abstract class BaseImageAdapter(
|
||||
Glide.with(this@loadBitmap)
|
||||
.asBitmap()
|
||||
.let {
|
||||
if (link.url.startsWith("file://")) {
|
||||
it.load(link.url)
|
||||
val fileUri = Uri.fromFile(File(link.url)).toString()
|
||||
val localFile = File(link.url)
|
||||
if (localFile.exists()) {
|
||||
it.load(localFile.absoluteFile)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package ani.dantotsu.media.manga.mangareader
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -45,7 +47,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
||||
loaded = true
|
||||
binding.selectorAutoText.text = chp.title
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if(model.loadMangaChapterImages(chp, m.selected!!)) {
|
||||
if(model.loadMangaChapterImages(chp, m.selected!!, m.nameMAL!!)) {
|
||||
val activity = currActivity()
|
||||
activity?.runOnUiThread {
|
||||
tryWith { dismiss() }
|
||||
@@ -63,6 +65,12 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
||||
val window = dialog?.window
|
||||
window?.statusBarColor = Color.TRANSPARENT
|
||||
val typedValue = TypedValue()
|
||||
val theme = requireContext().theme
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
window?.navigationBarColor = typedValue.data
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
|
||||
import ani.dantotsu.settings.ReaderSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
@@ -126,7 +127,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -215,6 +217,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
//check that index is not out of bounds (crash fix)
|
||||
if (media.selected!!.sourceIndex >= model.mangaReadSources!!.names.size) {
|
||||
media.selected!!.sourceIndex = 0
|
||||
}
|
||||
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex]
|
||||
|
||||
binding.mangaReaderTitle.text = media.userPreferredName
|
||||
@@ -311,7 +317,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!) }
|
||||
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!, media.nameMAL!!) }
|
||||
}
|
||||
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
@@ -700,6 +706,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
model.loadMangaChapterImages(
|
||||
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
||||
media.selected!!,
|
||||
media.nameMAL!!,
|
||||
false
|
||||
)
|
||||
loading = false
|
||||
|
||||
@@ -14,6 +14,7 @@ import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import ani.dantotsu.settings.NovelReaderSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.tryWith
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.vipulog.ebookreader.Book
|
||||
@@ -136,7 +137,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -32,25 +33,29 @@ class ListActivity : AppCompatActivity() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, typedValue, true)
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
val primaryColor = typedValue.data
|
||||
val typedValue2 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true)
|
||||
val primaryTextColor = typedValue2.data
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue2, true)
|
||||
val titleTextColor = typedValue2.data
|
||||
val typedValue3 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue3, true)
|
||||
val secondaryColor = typedValue3.data
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||
val primaryTextColor = typedValue3.data
|
||||
val typedValue4 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
|
||||
val secondaryTextColor = typedValue4.data
|
||||
|
||||
window.statusBarColor = primaryColor
|
||||
window.navigationBarColor = primaryColor
|
||||
binding.listTabLayout.setBackgroundColor(primaryColor)
|
||||
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
binding.listTabLayout.setTabTextColors(primaryTextColor, primaryTextColor)
|
||||
binding.listTitle.setTextColor(titleTextColor)
|
||||
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) {
|
||||
@@ -126,4 +131,4 @@ class ListActivity : AppCompatActivity() {
|
||||
popup.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.OtherDetailsViewModel
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
|
||||
class ListFragment : Fragment() {
|
||||
private var _binding: FragmentListBinding? = null
|
||||
|
||||
29
app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt
Normal file
29
app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
package ani.dantotsu.offline
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.databinding.FragmentOfflineBinding
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.statusBarHeight
|
||||
|
||||
class OfflineFragment : Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = FragmentOfflineBinding.inflate(inflater, container, false)
|
||||
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
binding.refreshButton.setOnClickListener {
|
||||
if (isOnline(requireContext())) {
|
||||
startMainActivity(requireActivity())
|
||||
}
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
@@ -95,8 +96,9 @@ object AppUpdater {
|
||||
|
||||
private fun compareVersion(version: String): Boolean {
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
if(BuildConfig.DEBUG) {
|
||||
return BuildConfig.VERSION_NAME != version
|
||||
}
|
||||
else {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return list.mapIndexed { i: Int, s: String ->
|
||||
@@ -141,7 +143,8 @@ object AppUpdater {
|
||||
-1
|
||||
}
|
||||
if (id == -1L) return true
|
||||
registerReceiver(
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
object : BroadcastReceiver() {
|
||||
@SuppressLint("Range")
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -170,7 +173,8 @@ object AppUpdater {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ani.dantotsu.others
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -46,6 +48,12 @@ open class CustomBottomDialog : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = BottomSheetCustomBinding.inflate(inflater, container, false)
|
||||
val window = dialog?.window
|
||||
window?.statusBarColor = Color.TRANSPARENT
|
||||
val typedValue = TypedValue()
|
||||
val theme = requireContext().theme
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
window?.navigationBarColor = typedValue.data
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.shareImage
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.toast
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
@@ -74,15 +75,16 @@ class ImageViewDialog : BottomSheetDialogFragment() {
|
||||
if (image2 != null) openLinkInBrowser(image2.url)
|
||||
true
|
||||
}
|
||||
val context = requireContext()
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val binding = _binding ?: return@launch
|
||||
|
||||
var bitmap = requireContext().loadBitmap_old(image, trans1 ?: listOf())
|
||||
var bitmap2 = if (image2 != null) requireContext().loadBitmap_old(image2, trans2 ?: listOf()) else null
|
||||
var bitmap = context.loadBitmap_old(image, trans1 ?: listOf())
|
||||
var bitmap2 = if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null
|
||||
if (bitmap == null) {
|
||||
bitmap = requireContext().loadBitmap(image, trans1 ?: listOf())
|
||||
bitmap2 = if (image2 != null) requireContext().loadBitmap(image2, trans2 ?: listOf()) else null
|
||||
bitmap = context.loadBitmap(image, trans1 ?: listOf())
|
||||
bitmap2 = if (image2 != null) context.loadBitmap(image2, trans2 ?: listOf()) else null
|
||||
}
|
||||
|
||||
bitmap = if (bitmap2 != null && bitmap != null) mergeBitmap(bitmap, bitmap2,) else bitmap
|
||||
|
||||
24
app/src/main/java/ani/dantotsu/others/LangSet.kt
Normal file
24
app/src/main/java/ani/dantotsu/others/LangSet.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package ani.dantotsu.others
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
|
||||
|
||||
class LangSet {
|
||||
companion object{
|
||||
fun setLocale(activity: Activity) {
|
||||
val useCursedLang = activity.getSharedPreferences("Dantotsu", Activity.MODE_PRIVATE).getBoolean("use_cursed_lang", false)
|
||||
if(!useCursedLang) return
|
||||
val locale = Locale("en", "rDW")
|
||||
Locale.setDefault(locale)
|
||||
val resources: Resources = activity.resources
|
||||
val config: Configuration = resources.configuration
|
||||
config.setLocale(locale)
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.ActivityImageSearchBinding
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -47,7 +48,8 @@ class ImageSearchActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityImageSearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package ani.dantotsu.parsers
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
@@ -10,14 +11,20 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.FileUrl
|
||||
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.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.ServiceDataSingleton
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
@@ -62,22 +69,54 @@ class AniyomiAdapter {
|
||||
|
||||
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
val extension: AnimeExtension.Installed
|
||||
var sourceLanguage = 0
|
||||
|
||||
init {
|
||||
this.extension = extension
|
||||
}
|
||||
|
||||
override val name = extension.name
|
||||
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()
|
||||
override suspend fun loadEpisodes(
|
||||
animeLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sAnime: SAnime
|
||||
): List<Episode> {
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
}
|
||||
if (source is AnimeCatalogueSource) {
|
||||
try {
|
||||
val res = source.getEpisodeList(sAnime)
|
||||
|
||||
// Sort episodes by episode_number
|
||||
val sortedEpisodes = res.sortedBy { it.episode_number }
|
||||
val sortedEpisodes = if (res[0].episode_number == -1f) {
|
||||
// Find the number in the string and sort by that number
|
||||
val sortedByStringNumber = res.sortedBy {
|
||||
val matchResult = "\\d+".toRegex().find(it.name)
|
||||
val number = matchResult?.value?.toFloat() ?: Float.MAX_VALUE
|
||||
it.episode_number = number // Store the found number in episode_number
|
||||
number
|
||||
}
|
||||
|
||||
// If there is no number, reverse the order and give them an incrementing number
|
||||
var incrementingNumber = 1f
|
||||
sortedByStringNumber.map {
|
||||
if (it.episode_number == Float.MAX_VALUE) {
|
||||
it.episode_number =
|
||||
incrementingNumber++ // Update episode_number with the incrementing number
|
||||
}
|
||||
it
|
||||
}
|
||||
} else {
|
||||
// Sort by the episode_number field
|
||||
res.sortedBy { it.episode_number }
|
||||
}
|
||||
|
||||
// Transform SEpisode objects to Episode objects
|
||||
|
||||
@@ -90,8 +129,17 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
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() as? AnimeCatalogueSource ?: return emptyList()
|
||||
override suspend fun loadVideoServers(
|
||||
episodeLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sEpisode: SEpisode
|
||||
): List<VideoServer> {
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? AnimeCatalogueSource ?: return emptyList()
|
||||
|
||||
return try {
|
||||
val videos = source.getVideoList(sEpisode)
|
||||
@@ -103,20 +151,25 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
|
||||
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor {
|
||||
return VideoServerPassthrough(server)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
|
||||
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? AnimeCatalogueSource ?: return emptyList()
|
||||
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()
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
@@ -150,7 +203,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
sEpisode.episode_number
|
||||
}
|
||||
return Episode(
|
||||
episodeNumberInt.toString(),
|
||||
if (episodeNumberInt.toInt() != -1) {
|
||||
episodeNumberInt.toString()
|
||||
} else {
|
||||
sEpisode.name
|
||||
},
|
||||
sEpisode.url,
|
||||
sEpisode.name,
|
||||
null,
|
||||
@@ -174,16 +231,28 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
val mangaCache = Injekt.get<MangaCache>()
|
||||
val extension: MangaExtension.Installed
|
||||
var sourceLanguage = 0
|
||||
|
||||
init {
|
||||
this.extension = extension
|
||||
}
|
||||
|
||||
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()
|
||||
override suspend fun loadChapters(
|
||||
mangaLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sManga: SManga
|
||||
): List<MangaChapter> {
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
|
||||
return try {
|
||||
val res = source.getChapterList(sManga)
|
||||
@@ -201,32 +270,79 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
|
||||
|
||||
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||
val source = extension.sources.first() as? HttpSource ?: return emptyList()
|
||||
|
||||
return coroutineScope {
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
var imageDataList: List<ImageData> = listOf()
|
||||
val ret = coroutineScope {
|
||||
try {
|
||||
println("source.name " + source.name)
|
||||
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 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))
|
||||
imageDataList += 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()
|
||||
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? {
|
||||
suspend fun imageList(chapterLink: String, sChapter: SChapter): List<ImageData>{
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
var imageDataList: List<ImageData> = listOf()
|
||||
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) {
|
||||
imageDataList += ImageData(page, source)
|
||||
}
|
||||
}
|
||||
|
||||
deferreds.awaitAll()
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger("loadImages Exception: $e")
|
||||
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
return imageDataList
|
||||
}
|
||||
|
||||
suspend fun fetchAndProcessImage(
|
||||
page: Page,
|
||||
httpSource: HttpSource,
|
||||
context: Context
|
||||
): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetch the image
|
||||
@@ -259,7 +375,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
@@ -274,7 +389,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
// Save the Bitmap using MediaStore API
|
||||
saveImage(bitmap, contentResolver, "image_${System.currentTimeMillis()}.jpg", Bitmap.CompressFormat.JPEG, 100)
|
||||
saveImage(
|
||||
bitmap,
|
||||
contentResolver,
|
||||
"image_${System.currentTimeMillis()}.jpg",
|
||||
Bitmap.CompressFormat.JPEG,
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
inputStream?.close()
|
||||
@@ -285,16 +406,28 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) {
|
||||
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")
|
||||
put(
|
||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime"
|
||||
)
|
||||
}
|
||||
|
||||
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
val uri: Uri? = contentResolver.insert(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
contentValues
|
||||
)
|
||||
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { os ->
|
||||
@@ -302,7 +435,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
val directory =
|
||||
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
@@ -319,9 +453,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val source = extension.sources.first() as? HttpSource ?: return emptyList()
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
|
||||
return try {
|
||||
val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
|
||||
@@ -330,7 +468,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
} catch (e: CloudflareBypassException) {
|
||||
logger("Exception in search: $e")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
@@ -390,30 +529,21 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
|
||||
|
||||
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,
|
||||
//},
|
||||
sChapter.name,
|
||||
null,
|
||||
sChapter.scanlator,
|
||||
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 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)
|
||||
@@ -423,10 +553,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
|
||||
var remainingTitle = title
|
||||
if (volumeNumber != null) {
|
||||
remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
remainingTitle =
|
||||
volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
}
|
||||
if (chapterNumber != null) {
|
||||
remainingTitle = chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
remainingTitle =
|
||||
chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
}
|
||||
|
||||
return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
|
||||
@@ -449,7 +581,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video {
|
||||
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video): ani.dantotsu.parsers.Video {
|
||||
// Find the number value from the .quality string
|
||||
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
|
||||
|
||||
@@ -479,9 +611,11 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||
// If the format is still undetermined, log an error or handle it appropriately
|
||||
if (format == null) {
|
||||
logger("Unknown video format: $videoUrl")
|
||||
throw Exception("Unknown video format")
|
||||
FirebaseCrashlytics.getInstance().recordException(Exception("Unknown video format: $videoUrl"))
|
||||
format = VideoType.CONTAINER
|
||||
}
|
||||
val headersMap: Map<String, String> = aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
|
||||
val headersMap: Map<String, String> =
|
||||
aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
|
||||
|
||||
|
||||
return ani.dantotsu.parsers.Video(
|
||||
@@ -494,7 +628,11 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||
|
||||
private fun getVideoType(fileName: String): VideoType? {
|
||||
return when {
|
||||
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
|
||||
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
|
||||
@@ -507,12 +645,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||
runBlocking {
|
||||
type = findSubtitleType(track.url)
|
||||
}
|
||||
return Subtitle(track.lang, track.url, type?: SubtitleType.SRT)
|
||||
return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
|
||||
}
|
||||
|
||||
private fun findSubtitleType(url: String): SubtitleType? {
|
||||
// First, try to determine the type based on the URL file extension
|
||||
var type: SubtitleType? = when {
|
||||
val type: SubtitleType? = when {
|
||||
url.endsWith(".vtt", true) -> SubtitleType.VTT
|
||||
url.endsWith(".ass", true) -> SubtitleType.ASS
|
||||
url.endsWith(".srt", true) -> SubtitleType.SRT
|
||||
|
||||
@@ -50,8 +50,8 @@ abstract class BaseParser {
|
||||
* Isn't necessary to override, but recommended, if you want to improve auto search results
|
||||
* **/
|
||||
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
|
||||
var response = loadSavedShowResponse(mediaObj.id)
|
||||
if (response != null) {
|
||||
var response: ShowResponse? = null//loadSavedShowResponse(mediaObj.id)
|
||||
if (response != null && this !is OfflineMangaParser) {
|
||||
saveShowResponse(mediaObj.id, response, true)
|
||||
} else {
|
||||
setUserText("Searching : ${mediaObj.mainName()}")
|
||||
|
||||
@@ -7,6 +7,7 @@ import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
abstract class WatchSources : BaseSources() {
|
||||
|
||||
@@ -23,14 +24,28 @@ abstract class WatchSources : BaseSources() {
|
||||
} ?: mutableMapOf()
|
||||
}
|
||||
|
||||
suspend fun loadEpisodes(i: Int, showLink: String, extra: Map<String, String>?, sAnime: SAnime?): MutableMap<String, Episode> {
|
||||
suspend fun loadEpisodes(
|
||||
i: Int,
|
||||
showLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sAnime: SAnime?
|
||||
): MutableMap<String, Episode> {
|
||||
println("finder333 $showLink")
|
||||
val map = mutableMapOf<String, Episode>()
|
||||
val parser = get(i)
|
||||
tryWithSuspend(true) {
|
||||
if (sAnime != null) {
|
||||
parser.loadEpisodes(showLink,extra, sAnime).forEach {
|
||||
map[it.number] = Episode(it.number, it.link, it.title, it.description, it.thumbnail, it.isFiller, extra = it.extra, sEpisode = it.sEpisode)
|
||||
parser.loadEpisodes(showLink, extra, sAnime).forEach {
|
||||
map[it.number] = Episode(
|
||||
it.number,
|
||||
it.link,
|
||||
it.title,
|
||||
it.description,
|
||||
it.thumbnail,
|
||||
it.isFiller,
|
||||
extra = it.extra,
|
||||
sEpisode = it.sEpisode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +57,7 @@ abstract class WatchSources : BaseSources() {
|
||||
abstract class MangaReadSources : BaseSources() {
|
||||
|
||||
override operator fun get(i: Int): MangaParser {
|
||||
return (list.getOrNull(i)?:list.firstOrNull())?.get?.value as? MangaParser
|
||||
return (list.getOrNull(i) ?: list.firstOrNull())?.get?.value as? MangaParser
|
||||
?: EmptyMangaParser()
|
||||
}
|
||||
|
||||
@@ -56,6 +71,7 @@ abstract class MangaReadSources : BaseSources() {
|
||||
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
|
||||
val map = mutableMapOf<String, MangaChapter>()
|
||||
val parser = get(i)
|
||||
|
||||
show.sManga?.let { sManga ->
|
||||
tryWithSuspend(true) {
|
||||
parser.loadChapters(show.link, show.extra, sManga).forEach {
|
||||
@@ -63,15 +79,28 @@ abstract class MangaReadSources : BaseSources() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if(show.sManga == null) {
|
||||
//must be downloaded
|
||||
if (show.sManga == null) {
|
||||
logger("sManga is null")
|
||||
}
|
||||
if (parser is OfflineMangaParser && show.sManga == null) {
|
||||
tryWithSuspend(true) {
|
||||
// Since we've checked, we can safely cast parser to OfflineMangaParser and call its methods
|
||||
parser.loadChapters(show.link, show.extra, SManga.create()).forEach {
|
||||
map[it.number] = MangaChapter(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger("Parser is not an instance of OfflineMangaParser")
|
||||
}
|
||||
|
||||
|
||||
logger("map size ${map.size}")
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NovelReadSources : BaseSources(){
|
||||
abstract class NovelReadSources : BaseSources() {
|
||||
override operator fun get(i: Int): NovelParser? {
|
||||
return if (list.isNotEmpty()) {
|
||||
(list.getOrNull(i) ?: list[0]).get.value as NovelParser
|
||||
@@ -87,7 +116,7 @@ class EmptyNovelParser : NovelParser() {
|
||||
override val volumeRegex: Regex = Regex("")
|
||||
|
||||
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
|
||||
return Book("","", null, emptyList()) // Return an empty Book object or some default value
|
||||
return Book("", "", null, emptyList()) // Return an empty Book object or some default value
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
|
||||
@@ -67,7 +67,7 @@ data class MangaChapter(
|
||||
//Self-Descriptive
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
|
||||
val scanlator: String? = null,
|
||||
val sChapter: SChapter,
|
||||
)
|
||||
|
||||
@@ -81,8 +81,8 @@ data class MangaImage(
|
||||
|
||||
val useTransformation: Boolean = false,
|
||||
|
||||
val page: Page
|
||||
val page: Page? = null,
|
||||
) : Serializable{
|
||||
constructor(url: String,useTransformation: Boolean=false, page: Page)
|
||||
constructor(url: String,useTransformation: Boolean=false, page: Page? = null)
|
||||
: this(FileUrl(url),useTransformation, page)
|
||||
}
|
||||
|
||||
@@ -7,16 +7,19 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
object MangaSources : MangaReadSources() {
|
||||
// Instantiate the static parser
|
||||
private val offlineMangaParser by lazy { OfflineMangaParser() }
|
||||
|
||||
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)
|
||||
list = createParsersFromExtensions(initialExtensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
|
||||
|
||||
// Update as StateFlow emits new values
|
||||
fromExtensions.collect { extensions ->
|
||||
list = createParsersFromExtensions(extensions)
|
||||
list = createParsersFromExtensions(extensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
Normal file
77
app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
Normal file
@@ -0,0 +1,77 @@
|
||||
package ani.dantotsu.parsers
|
||||
|
||||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class OfflineMangaParser: MangaParser() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
override val hostUrl: String = "Offline"
|
||||
override val name: String = "Offline"
|
||||
override val saveName: String = "Offline"
|
||||
override suspend fun loadChapters(
|
||||
mangaLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sManga: SManga
|
||||
): List<MangaChapter> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$mangaLink"
|
||||
)
|
||||
//get all of the folder names and add them to the list
|
||||
val chapters = mutableListOf<MangaChapter>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (it.isDirectory) {
|
||||
val chapter = MangaChapter(it.name, "$mangaLink/${it.name}", it.name, null, null, SChapter.create())
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
chapters.sortBy { MangaNameAdapter.findChapterNumber(it.number) }
|
||||
return chapters
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$chapterLink"
|
||||
)
|
||||
val images = mutableListOf<MangaImage>()
|
||||
if (directory.exists()) {
|
||||
directory.listFiles()?.forEach {
|
||||
if (it.isFile) {
|
||||
val image = MangaImage(it.absolutePath, false, null)
|
||||
images.add(image)
|
||||
}
|
||||
}
|
||||
return images
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
|
||||
val returnTitles: MutableList<String> = mutableListOf()
|
||||
for (title in titles) {
|
||||
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
|
||||
returnTitles.add(title)
|
||||
}
|
||||
}
|
||||
val returnList: MutableList<ShowResponse> = mutableListOf()
|
||||
for (title in returnTitles) {
|
||||
returnList.add(ShowResponse(title, title, title))
|
||||
}
|
||||
return returnList
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,10 +49,10 @@ class AnimeExtensionsFragment : Fragment(),
|
||||
): View {
|
||||
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = true
|
||||
binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = false
|
||||
binding.allAnimeExtensionsRecyclerView.adapter = adapter
|
||||
binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||
(binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = false
|
||||
(binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.pagerFlow.collectLatest {
|
||||
@@ -60,6 +60,8 @@ class AnimeExtensionsFragment : Fragment(),
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.invalidatePager() // Force a refresh of the pager
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -13,26 +13,9 @@ class DevelopersDialogFragment : BottomSheetDialogFragment() {
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val developers = arrayOf(
|
||||
Developer("vorobyovgabriel","https://avatars.githubusercontent.com/u/99561687?s=120&v=4","Owner","https://github.com/vorobyovgabriel"),
|
||||
Developer("brahmkshtriya","https://avatars.githubusercontent.com/u/69040506?s=120&v=4","Maintainer","https://github.com/brahmkshatriya"),
|
||||
Developer("jeelpatel231","https://avatars.githubusercontent.com/u/33726155?s=120&v=4","Contributor","https://github.com/jeelpatel231"),
|
||||
Developer("blatzar","https://avatars.githubusercontent.com/u/46196380?s=120&v=4","Contributor","https://github.com/Blatzar"),
|
||||
Developer("bilibox","https://avatars.githubusercontent.com/u/1800580?s=120&v=4","Contributor","https://github.com/Bilibox"),
|
||||
Developer("sutslec","https://avatars.githubusercontent.com/u/27722281?s=120&v=4","Contributor","https://github.com/Sutslec"),
|
||||
Developer("4jx","https://avatars.githubusercontent.com/u/79868816?s=120&v=4","Contributor","https://github.com/4JX"),
|
||||
Developer("xtrm-en","https://avatars.githubusercontent.com/u/26600206?s=120&v=4","Contributor","https://github.com/xtrm-en"),
|
||||
Developer("scrazzz","https://avatars.githubusercontent.com/u/70033559?s=120&v=4","Contributor","https://github.com/scrazzz"),
|
||||
Developer("defcoding","https://avatars.githubusercontent.com/u/39608887?s=120&v=4","Contributor","https://github.com/defcoding"),
|
||||
Developer("adolar0042","https://avatars.githubusercontent.com/u/39769465?s=120&v=4","Contributor","https://github.com/adolar0042"),
|
||||
Developer("diegopyl1209","https://avatars.githubusercontent.com/u/80992641?s=120&v=4","Contributor","https://github.com/diegopyl1209"),
|
||||
Developer("sreekrishna2001","https://avatars.githubusercontent.com/u/67505103?s=120&v=4","Contributor","https://github.com/Sreekrishna2001"),
|
||||
Developer("riimuru","https://avatars.githubusercontent.com/u/57333995?s=120&v=4","Contributor","https://github.com/riimuru"),
|
||||
Developer("vu nguyen","https://avatars.githubusercontent.com/u/68330291?s=120&v=4","Contributor","https://github.com/hoangvu12"),
|
||||
Developer("animejeff","https://avatars.githubusercontent.com/u/101831300?s=120&v=4","Contributor","https://github.com/AnimeJeff"),
|
||||
Developer("antonydp","https://avatars.githubusercontent.com/u/38143733?s=120&v=4","Contributor","https://github.com/antonydp"),
|
||||
Developer("tobybridle","https://avatars.githubusercontent.com/u/52335751?s=120&v=4","Contributor","https://github.com/TobyBridle"),
|
||||
Developer("enimax","https://avatars.githubusercontent.com/u/107899019?s=120&v=4","Contributor","https://github.com/enimax-anime"),
|
||||
Developer("vipulog","https://avatars.githubusercontent.com/u/90324465?s=120&v=4","Contributor","https://github.com/VipulOG")
|
||||
Developer("rebelonion","https://avatars.githubusercontent.com/u/87634197?v=4","Owner and Maintainer","https://github.com/rebelonion"),
|
||||
Developer("Wai What", "https://avatars.githubusercontent.com/u/149729762?v=4", "Icon Designer", "https://github.com/WaiWhat"),
|
||||
Developer("Aayush262", "https://avatars.githubusercontent.com/u/99584765?v=4", "Contributor", "https://github.com/aayush2622"),
|
||||
)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
||||
@@ -1,55 +1,32 @@
|
||||
package ani.dantotsu.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build.*
|
||||
import android.os.Build.VERSION.*
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.AutoCompleteTextView
|
||||
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.appcompat.widget.PopupMenu
|
||||
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 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 ani.dantotsu.themes.ThemeManager
|
||||
import com.bumptech.glide.Glide
|
||||
import ani.dantotsu.others.LangSet
|
||||
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() {
|
||||
private val restartMainActivity = object : OnBackPressedCallback(false) {
|
||||
@@ -61,7 +38,8 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityExtensionsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -94,21 +72,20 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
}.attach()
|
||||
|
||||
|
||||
val searchView: SearchView = findViewById(R.id.searchView)
|
||||
val searchView: AutoCompleteTextView = findViewById(R.id.searchViewText)
|
||||
|
||||
val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
searchView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}")
|
||||
if (currentFragment is SearchQueryHandler) {
|
||||
currentFragment.updateContentBasedOnQuery(newText)
|
||||
currentFragment.updateContentBasedOnQuery(s?.toString()?.trim())
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -116,18 +93,18 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
initActivity(this)
|
||||
|
||||
|
||||
binding.languageselect.setOnClickListener {
|
||||
val popup = PopupMenu(this, it)
|
||||
|
||||
popup.inflate(R.menu.launguage_selector_menu)
|
||||
popup.show()
|
||||
}
|
||||
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, restartMainActivity)
|
||||
|
||||
binding.settingsBack.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ActivityFaqBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
|
||||
class FAQActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityFaqBinding
|
||||
@@ -105,7 +106,8 @@ class FAQActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityFaqBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ani.dantotsu.settings
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
@@ -7,8 +8,10 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -17,10 +20,15 @@ import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
@@ -30,62 +38,129 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class InstalledAnimeExtensionsFragment : Fragment() {
|
||||
|
||||
|
||||
private var _binding: FragmentAnimeExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
|
||||
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
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
|
||||
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 ->
|
||||
FirebaseCrashlytics.getInstance().recordException(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: ${error.message}")
|
||||
.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())
|
||||
// Move the fragment transaction here
|
||||
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||
val activity = requireActivity() as ExtensionsActivity
|
||||
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.VISIBLE
|
||||
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.VISIBLE
|
||||
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.VISIBLE
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
View.GONE
|
||||
}
|
||||
)
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
return@setNegativeButton
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
animeExtensionManager.uninstallExtension(pkg.pkgName)
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||
val activity = requireActivity() as ExtensionsActivity
|
||||
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.VISIBLE
|
||||
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.VISIBLE
|
||||
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.VISIBLE
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
View.GONE
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
// Hide ViewPager2 and TabLayout
|
||||
val activity = requireActivity() as ExtensionsActivity
|
||||
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.GONE
|
||||
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.GONE
|
||||
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.GONE
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = View.VISIBLE
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}, skipIcons)
|
||||
},
|
||||
{ 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 ->
|
||||
FirebaseCrashlytics.getInstance().recordException(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: ${error.message}")
|
||||
.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
|
||||
)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -114,6 +189,7 @@ class InstalledAnimeExtensionsFragment : Fragment() {
|
||||
|
||||
|
||||
private class AnimeExtensionsAdapter(
|
||||
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
|
||||
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
|
||||
skipIcons: Boolean
|
||||
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
|
||||
@@ -133,31 +209,37 @@ class InstalledAnimeExtensionsFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||
val nsfw = if (extension.isNsfw) {
|
||||
"(18+)"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
holder.extensionVersionTextView.text = "${extension.versionName} $nsfw"
|
||||
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
|
||||
)
|
||||
)
|
||||
holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24)
|
||||
} else {
|
||||
holder.closeTextView.text = "Uninstall"
|
||||
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
|
||||
}
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
}
|
||||
holder.settingsImageView.setOnClickListener {
|
||||
onSettingsClicked(extension)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
val extensionVersionTextView: TextView = view.findViewById(R.id.extensionVersionTextView)
|
||||
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -180,5 +262,4 @@ class InstalledAnimeExtensionsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package ani.dantotsu.settings
|
||||
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
@@ -8,8 +9,10 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -18,13 +21,18 @@ import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -37,6 +45,66 @@ class InstalledMangaExtensionsFragment : Fragment() {
|
||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
|
||||
private val extensionsAdapter = MangaExtensionsAdapter({ pkg ->
|
||||
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||
val activity = requireActivity() as ExtensionsActivity
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = visibility
|
||||
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = visibility
|
||||
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = visibility
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { it.lang }.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
changeUIVisibility(true)
|
||||
return@setNegativeButton
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
// Hide ViewPager2 and TabLayout
|
||||
changeUIVisibility(false)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
{ pkg ->
|
||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||
val context = requireContext() // Store context in a variable
|
||||
val notificationManager =
|
||||
@@ -115,6 +183,7 @@ class InstalledMangaExtensionsFragment : Fragment() {
|
||||
|
||||
|
||||
private class MangaExtensionsAdapter(
|
||||
private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
|
||||
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
|
||||
skipIcons: Boolean
|
||||
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
|
||||
@@ -135,30 +204,35 @@ class InstalledMangaExtensionsFragment : Fragment() {
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||
val nsfw = if (extension.isNsfw) {
|
||||
"(18+)"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
holder.extensionVersionTextView.text = "${extension.versionName} $nsfw"
|
||||
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
|
||||
)
|
||||
)
|
||||
holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24)
|
||||
} else {
|
||||
holder.closeTextView.text = "Uninstall"
|
||||
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
|
||||
}
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
}
|
||||
holder.settingsImageView.setOnClickListener {
|
||||
onSettingsClicked(extension)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||
val extensionVersionTextView: TextView = view.findViewById(R.id.extensionVersionTextView)
|
||||
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||
val closeTextView: ImageView = view.findViewById(R.id.closeTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
||||
@@ -25,6 +26,7 @@ import ani.dantotsu.settings.paging.MangaExtensionAdapter
|
||||
import ani.dantotsu.settings.paging.MangaExtensionsViewModel
|
||||
import ani.dantotsu.settings.paging.MangaExtensionsViewModelFactory
|
||||
import ani.dantotsu.settings.paging.OnMangaInstallClickListener
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
class MangaExtensionsFragment : Fragment(),
|
||||
@@ -50,17 +52,19 @@ class MangaExtensionsFragment : Fragment(),
|
||||
): View {
|
||||
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = true
|
||||
binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = false
|
||||
binding.allMangaExtensionsRecyclerView.adapter = adapter
|
||||
binding.allMangaExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||
(binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = false
|
||||
(binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.pagerFlow.collectLatest {
|
||||
adapter.submitData(it)
|
||||
viewModel.pagerFlow.collectLatest { pagingData ->
|
||||
adapter.submitData(pagingData)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.invalidatePager() // Force a refresh of the pager
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ data class PlayerSettings(
|
||||
var focusPause: Boolean = true,
|
||||
var gestures: Boolean = true,
|
||||
var doubleTap: Boolean = true,
|
||||
var fastforward: Boolean = true,
|
||||
var seekTime: Int = 10,
|
||||
var skipTime: Int = 85,
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -31,7 +32,8 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityPlayerSettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -183,7 +185,11 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
||||
settings.doubleTap = isChecked
|
||||
saveData(player, settings)
|
||||
}
|
||||
|
||||
binding.playerSettingsFastForward.isChecked = settings.fastforward
|
||||
binding.playerSettingsFastForward.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.fastforward = isChecked
|
||||
saveData(player, settings)
|
||||
}
|
||||
binding.playerSettingsSeekTime.value = settings.seekTime.toFloat()
|
||||
binding.playerSettingsSeekTime.addOnChangeListener { _, value, _ ->
|
||||
settings.seekTime = value.toInt()
|
||||
|
||||
@@ -13,13 +13,15 @@ import ani.dantotsu.saveData
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
|
||||
class ReaderSettingsActivity : AppCompatActivity() {
|
||||
lateinit var binding: ActivityReaderSettingsBinding
|
||||
private val reader = "reader_settings"
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityReaderSettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
@@ -53,11 +54,13 @@ class SettingsActivity : AppCompatActivity() {
|
||||
lateinit var binding: ActivitySettingsBinding
|
||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
||||
private val networkPreferences = Injekt.get<NetworkPreferences>()
|
||||
private var cursedCounter = 0
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -105,6 +108,12 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
restartApp()
|
||||
}
|
||||
|
||||
binding.settingsUseOLED.isChecked = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_oled", false)
|
||||
binding.settingsUseOLED.setOnCheckedChangeListener { _, isChecked ->
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_oled", isChecked).apply()
|
||||
restartApp()
|
||||
}
|
||||
|
||||
val themeString = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!!
|
||||
binding.themeSwitcher.setText(themeString.substring(0, 1) + themeString.substring(1).lowercase())
|
||||
|
||||
@@ -161,6 +170,11 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked ->
|
||||
saveData("skip_extension_icons", isChecked)
|
||||
}
|
||||
binding.NSFWExtension.isChecked = loadData("NFSWExtension") ?: true
|
||||
binding.NSFWExtension.setOnCheckedChangeListener { _, isChecked ->
|
||||
saveData("NFSWExtension", isChecked)
|
||||
|
||||
}
|
||||
|
||||
binding.userAgent.setText(networkPreferences.defaultUserAgent().get())
|
||||
binding.userAgent.setOnEditorActionListener { _, _, _ ->
|
||||
@@ -168,7 +182,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
true
|
||||
}
|
||||
|
||||
val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan")
|
||||
val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan", "Libre")
|
||||
binding.settingsExtensionDns.setText(exDns[networkPreferences.dohProvider().get()], false)
|
||||
binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns))
|
||||
binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
|
||||
@@ -383,8 +397,15 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
val array = resources.getStringArray(R.array.tips)
|
||||
|
||||
binding.settingsLogo.setSafeOnClickListener {
|
||||
cursedCounter++
|
||||
(binding.settingsLogo.drawable as Animatable).start()
|
||||
snackString(array[(Math.random() * array.size).toInt()], this)
|
||||
if (cursedCounter % 7 == 0){
|
||||
snackString("youwu have been cuwsed :pwayge:")
|
||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_cursed_lang", true).apply()
|
||||
} else{
|
||||
snackString(array[(Math.random() * array.size).toInt()], this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
binding.settingsDev.setOnClickListener {
|
||||
@@ -408,7 +429,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
}
|
||||
}
|
||||
|
||||
var curTime = loadData<Int>("subscriptions_time_r") ?: defaultTime
|
||||
var curTime = loadData<Int>("subscriptions_time_s") ?: defaultTime
|
||||
val timeNames = timeMinutes.map {
|
||||
val mins = it % 60
|
||||
val hours = it / 60
|
||||
@@ -421,7 +442,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||
speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i ->
|
||||
curTime = i
|
||||
binding.settingsSubscriptionsTime.text = getString(R.string.subscriptions_checking_time_s, timeNames[i])
|
||||
saveData("subscriptions_time_r", curTime)
|
||||
saveData("subscriptions_time_s", curTime)
|
||||
dialog.dismiss()
|
||||
startSubscription(true)
|
||||
}.show()
|
||||
|
||||
@@ -3,19 +3,24 @@ package ani.dantotsu.settings
|
||||
import android.app.DownloadManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Switch
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.others.imagesearch.ImageSearchActivity
|
||||
import ani.dantotsu.databinding.BottomSheetSettingsBinding
|
||||
import ani.dantotsu.download.DownloadContainerActivity
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
|
||||
|
||||
class SettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
class SettingsDialogFragment(val pageType: PageType) : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
@@ -26,6 +31,12 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val window = dialog?.window
|
||||
window?.statusBarColor = Color.CYAN
|
||||
val typedValue = TypedValue()
|
||||
val theme = requireContext().theme
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
window?.navigationBarColor = typedValue.data
|
||||
|
||||
if (Anilist.token != null) {
|
||||
binding.settingsLogin.setText(R.string.logout)
|
||||
@@ -62,18 +73,42 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
binding.settingsDownloads.setSafeOnClickListener {
|
||||
try {
|
||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null)
|
||||
startActivity(
|
||||
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
|
||||
val parentDirectory = arrayOfFiles[1].toString()
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
|
||||
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
toast(getString(R.string.file_manager_not_found))
|
||||
when(pageType) {
|
||||
PageType.MANGA -> {
|
||||
val intent = Intent(activity, DownloadContainerActivity::class.java)
|
||||
intent.putExtra("FRAGMENT_CLASS_NAME", OfflineMangaFragment::class.java.name)
|
||||
startActivity(intent)
|
||||
}
|
||||
PageType.ANIME -> {
|
||||
try {
|
||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null)
|
||||
startActivity(
|
||||
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
|
||||
val parentDirectory = arrayOfFiles[1].toString()
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
|
||||
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
toast(getString(R.string.file_manager_not_found))
|
||||
}
|
||||
}
|
||||
PageType.HOME -> {
|
||||
try {
|
||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null)
|
||||
startActivity(
|
||||
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
|
||||
val parentDirectory = arrayOfFiles[1].toString()
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.parse(parentDirectory), "resource/folder")
|
||||
} else Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
toast(getString(R.string.file_manager_not_found))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@@ -82,4 +117,10 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object{
|
||||
enum class PageType{
|
||||
MANGA, ANIME, HOME
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.core.view.updateLayoutParams
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.ActivityUserInterfaceSettingsBinding
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
class UserInterfaceSettingsActivity : AppCompatActivity() {
|
||||
@@ -16,7 +17,8 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
|
||||
private val ui = "ui_settings"
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityUserInterfaceSettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package ani.dantotsu.settings.extensionprefs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.DialogPreference
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.forEach
|
||||
import androidx.preference.getOnBindEditTextListener
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.settings.ExtensionsActivity
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.kanade.tachiyomi.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||
import eu.kanade.tachiyomi.source.anime.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceScreen = populateAnimePreferenceScreen()
|
||||
//set background color
|
||||
val color = TypedValue()
|
||||
requireContext().theme.resolveAttribute(com.google.android.material.R.attr.backgroundColor, color, true)
|
||||
view?.setBackgroundColor(color.data)
|
||||
}
|
||||
private var onCloseAction: (() -> Unit)? = null
|
||||
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
onCloseAction?.invoke()
|
||||
}
|
||||
|
||||
fun populateAnimePreferenceScreen(): PreferenceScreen {
|
||||
val sourceId = requireArguments().getLong(SOURCE_ID)
|
||||
val source = Injekt.get<AnimeSourceManager>().get(sourceId)!!
|
||||
check(source is ConfigurableAnimeSource)
|
||||
val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||
val dataStore = SharedPreferencesDataStore(sharedPreferences)
|
||||
preferenceManager.preferenceDataStore = dataStore
|
||||
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
||||
source.setupPreferenceScreen(sourceScreen)
|
||||
sourceScreen.forEach { pref ->
|
||||
pref.isIconSpaceReserved = false
|
||||
if (pref is DialogPreference) {
|
||||
pref.dialogTitle = pref.title
|
||||
println("pref.dialogTitle: ${pref.dialogTitle}")
|
||||
}
|
||||
for (entry in sharedPreferences.all.entries) {
|
||||
Log.d("Preferences", "Key: ${entry.key}, Value: ${entry.value}")
|
||||
}
|
||||
|
||||
// Apply incognito IME for EditTextPreference
|
||||
if (pref is EditTextPreference) {
|
||||
val setListener = pref.getOnBindEditTextListener()
|
||||
pref.setOnBindEditTextListener {
|
||||
setListener?.onBindEditText(it)
|
||||
it.setIncognito(lifecycleScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sourceScreen
|
||||
}
|
||||
fun getInstance(sourceId: Long, onCloseAction: (() -> Unit)? = null): AnimeSourcePreferencesFragment {
|
||||
val fragment = AnimeSourcePreferencesFragment()
|
||||
fragment.arguments = bundleOf(SOURCE_ID to sourceId)
|
||||
fragment.onCloseAction = onCloseAction
|
||||
return fragment
|
||||
}
|
||||
|
||||
companion object { //idk why it needs both
|
||||
private const val SOURCE_ID = "source_id"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package ani.dantotsu.settings.extensionprefs
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.DialogPreference
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.forEach
|
||||
import androidx.preference.getOnBindEditTextListener
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.settings.ExtensionsActivity
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.kanade.tachiyomi.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.manga.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaSourcePreferencesFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceScreen = populateMangaPreferenceScreen()
|
||||
}
|
||||
private var onCloseAction: (() -> Unit)? = null
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
onCloseAction?.invoke()
|
||||
|
||||
}
|
||||
|
||||
fun populateMangaPreferenceScreen(): PreferenceScreen {
|
||||
val sourceId = requireArguments().getLong(SOURCE_ID)
|
||||
val source = Injekt.get<MangaSourceManager>().get(sourceId)!!
|
||||
check(source is ConfigurableSource)
|
||||
val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||
val dataStore = SharedPreferencesDataStore(sharedPreferences)
|
||||
preferenceManager.preferenceDataStore = dataStore
|
||||
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
||||
source.setupPreferenceScreen(sourceScreen)
|
||||
sourceScreen.forEach { pref ->
|
||||
pref.isIconSpaceReserved = false
|
||||
if (pref is DialogPreference) {
|
||||
pref.dialogTitle = pref.title
|
||||
println("pref.dialogTitle: ${pref.dialogTitle}")
|
||||
}
|
||||
|
||||
// Apply incognito IME for EditTextPreference
|
||||
if (pref is EditTextPreference) {
|
||||
val setListener = pref.getOnBindEditTextListener()
|
||||
pref.setOnBindEditTextListener {
|
||||
setListener?.onBindEditText(it)
|
||||
it.setIncognito(lifecycleScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sourceScreen
|
||||
}
|
||||
fun getInstance(sourceId: Long, onCloseAction: (() -> Unit)? = null): MangaSourcePreferencesFragment {
|
||||
val fragment = MangaSourcePreferencesFragment()
|
||||
fragment.arguments = bundleOf(SOURCE_ID to sourceId)
|
||||
fragment.onCloseAction = onCloseAction
|
||||
return fragment
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SOURCE_ID = "source_id"
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import androidx.paging.PagingState
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.databinding.ItemExtensionAllBinding
|
||||
import ani.dantotsu.loadData
|
||||
import com.bumptech.glide.Glide
|
||||
@@ -77,21 +78,26 @@ class AnimeExtensionPagingSource(
|
||||
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
|
||||
val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
|
||||
val query = searchQuery.first()
|
||||
var isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true
|
||||
val filteredExtensions = if (query.isEmpty()) {
|
||||
availableExtensions
|
||||
} else {
|
||||
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
|
||||
val filternfsw = if(isNsfwEnabled) {
|
||||
filteredExtensions
|
||||
} else {
|
||||
filteredExtensions.filterNot { it.isNsfw }
|
||||
}
|
||||
return try {
|
||||
val sublist = filteredExtensions.subList(
|
||||
val sublist = filternfsw.subList(
|
||||
fromIndex = position,
|
||||
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size)
|
||||
toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size)
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = sublist,
|
||||
prevKey = if (position == 0) null else position - params.loadSize,
|
||||
nextKey = if (position + params.loadSize >= filteredExtensions.size) null else position + params.loadSize
|
||||
nextKey = if (position + params.loadSize >= filternfsw.size) null else position + params.loadSize
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
@@ -151,8 +157,14 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen
|
||||
}
|
||||
}
|
||||
val extensionIconImageView: ImageView = binding.extensionIconImageView
|
||||
fun bind(extension: AnimeExtension.Available) {
|
||||
fun bind(extension: AnimeExtension.Available) {
|
||||
val nsfw = if (extension.isNsfw) {
|
||||
"(18+)"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
binding.extensionNameTextView.text = extension.name
|
||||
binding.extensionVersionTextView.text = "${extension.versionName} $nsfw"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.paging.PagingState
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.databinding.ItemExtensionAllBinding
|
||||
import ani.dantotsu.loadData
|
||||
import com.bumptech.glide.Glide
|
||||
@@ -81,21 +82,26 @@ class MangaExtensionPagingSource(
|
||||
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
|
||||
val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
|
||||
val query = searchQuery.first()
|
||||
var isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: false
|
||||
val filteredExtensions = if (query.isEmpty()) {
|
||||
availableExtensions
|
||||
} else {
|
||||
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
|
||||
val filternfsw = if(isNsfwEnabled) {
|
||||
filteredExtensions
|
||||
} else {
|
||||
filteredExtensions.filterNot { it.isNsfw }
|
||||
}
|
||||
return try {
|
||||
val sublist = filteredExtensions.subList(
|
||||
val sublist = filternfsw.subList(
|
||||
fromIndex = position,
|
||||
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size)
|
||||
toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size)
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = sublist,
|
||||
prevKey = if (position == 0) null else position - params.loadSize,
|
||||
nextKey = if (position + params.loadSize >= filteredExtensions.size) null else position + params.loadSize
|
||||
nextKey = if (position + params.loadSize >= filternfsw.size) null else position + params.loadSize
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
@@ -117,12 +123,10 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
|
||||
companion object {
|
||||
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MangaExtension.Available>() {
|
||||
override fun areItemsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean {
|
||||
// Your logic here
|
||||
return oldItem.pkgName == newItem.pkgName
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean {
|
||||
// Your logic here
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
@@ -156,7 +160,13 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen
|
||||
}
|
||||
val extensionIconImageView: ImageView = binding.extensionIconImageView
|
||||
fun bind(extension: MangaExtension.Available) {
|
||||
val nsfw = if (extension.isNsfw) {
|
||||
"(18+)"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
binding.extensionNameTextView.text = extension.name
|
||||
binding.extensionVersionTextView.text = "${extension.versionName} $nsfw"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class AlarmReceiver : BroadcastReceiver() {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val curTime = loadData<Int>("subscriptions_time_r", context) ?: defaultTime
|
||||
val curTime = loadData<Int>("subscriptions_time_s", context) ?: defaultTime
|
||||
|
||||
if (timeMinutes[curTime] > 0)
|
||||
alarmManager.setRepeating(
|
||||
|
||||
@@ -16,8 +16,8 @@ import kotlinx.coroutines.launch
|
||||
@SuppressLint("MissingPermission")
|
||||
class Subscription {
|
||||
companion object {
|
||||
const val defaultTime = 3
|
||||
val timeMinutes = arrayOf(0L, 120, 180, 240, 360, 480, 720, 1440)
|
||||
const val defaultTime = 1
|
||||
val timeMinutes = arrayOf(0L, 720, 1440)
|
||||
|
||||
private var alreadyStarted = false
|
||||
fun Context.startSubscription(force: Boolean = false) {
|
||||
|
||||
@@ -22,7 +22,7 @@ class SubscriptionWorker(val context: Context, params: WorkerParameters) : Corou
|
||||
|
||||
private const val SUBSCRIPTION_WORK_NAME = "work_subscription"
|
||||
fun enqueue(context: Context) {
|
||||
val curTime = loadData<Int>("subscriptions_time_r") ?: defaultTime
|
||||
val curTime = loadData<Int>("subscriptions_time_s") ?: defaultTime
|
||||
if(timeMinutes[curTime]>0L) {
|
||||
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
||||
val periodicSyncDataWork = PeriodicWorkRequest.Builder(
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
package ani.dantotsu.themes
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import ani.dantotsu.R
|
||||
|
||||
class ThemeManager(private val context: Context) {
|
||||
fun applyTheme() {
|
||||
val useOLED = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_oled", false) && isDarkThemeActive(context)
|
||||
if(context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false)){
|
||||
return
|
||||
}
|
||||
when (context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!!) {
|
||||
"PURPLE" -> {
|
||||
context.setTheme(R.style.Theme_Dantotsu_Purple)
|
||||
}
|
||||
"BLUE" -> {
|
||||
context.setTheme(R.style.Theme_Dantotsu_Blue)
|
||||
}
|
||||
"GREEN" -> {
|
||||
context.setTheme(R.style.Theme_Dantotsu_Green)
|
||||
}
|
||||
"PINK" -> {
|
||||
context.setTheme(R.style.Theme_Dantotsu_Pink)
|
||||
}
|
||||
"RED" -> {
|
||||
context.setTheme(R.style.Theme_Dantotsu_Red)
|
||||
}
|
||||
"LAVENDER" -> {
|
||||
context.setTheme(R.style.Theme_Dantotsu_Lavender)
|
||||
}
|
||||
"MONOCHROME (BETA)" -> {
|
||||
context.setTheme(R.style.Theme_Dantotsu_Monochrome)
|
||||
}
|
||||
else -> {
|
||||
context.setTheme(R.style.Theme_Dantotsu_Purple)
|
||||
}
|
||||
val theme = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!!
|
||||
|
||||
val themeToApply = when (theme) {
|
||||
"PURPLE" -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple
|
||||
"BLUE" -> if (useOLED) R.style.Theme_Dantotsu_BlueOLED else R.style.Theme_Dantotsu_Blue
|
||||
"GREEN" -> if (useOLED) R.style.Theme_Dantotsu_GreenOLED else R.style.Theme_Dantotsu_Green
|
||||
"PINK" -> if (useOLED) R.style.Theme_Dantotsu_PinkOLED else R.style.Theme_Dantotsu_Pink
|
||||
"RED" -> if (useOLED) R.style.Theme_Dantotsu_RedOLED else R.style.Theme_Dantotsu_Red
|
||||
"LAVENDER" -> if (useOLED) R.style.Theme_Dantotsu_LavenderOLED else R.style.Theme_Dantotsu_Lavender
|
||||
"MONOCHROME (BETA)" -> if (useOLED) R.style.Theme_Dantotsu_MonochromeOLED else R.style.Theme_Dantotsu_Monochrome
|
||||
"SAIKOU" -> if (useOLED) R.style.Theme_Dantotsu_SaikouOLED else R.style.Theme_Dantotsu_Saikou
|
||||
else -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple
|
||||
}
|
||||
|
||||
context.setTheme(themeToApply)
|
||||
}
|
||||
|
||||
private fun isDarkThemeActive(context: Context): Boolean {
|
||||
return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
||||
Configuration.UI_MODE_NIGHT_YES -> true
|
||||
Configuration.UI_MODE_NIGHT_NO -> false
|
||||
Configuration.UI_MODE_NIGHT_UNDEFINED -> false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object{
|
||||
enum class Theme(val theme: String) {
|
||||
PURPLE("PURPLE"),
|
||||
@@ -44,7 +45,8 @@ class ThemeManager(private val context: Context) {
|
||||
PINK("PINK"),
|
||||
RED("RED"),
|
||||
LAVENDER("LAVENDER"),
|
||||
MONOCHROME("MONOCHROME (BETA)");
|
||||
MONOCHROME("MONOCHROME (BETA)"),
|
||||
SAIKOU("SAIKOU");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): Theme {
|
||||
@@ -53,4 +55,4 @@ class ThemeManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.animesource
|
||||
|
||||
/**
|
||||
* A source that explicitly doesn't require traffic considerations.
|
||||
*
|
||||
* This typically applies for self-hosted sources.
|
||||
*/
|
||||
interface UnmeteredSource
|
||||
@@ -0,0 +1,68 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||
return prefs.getBoolean(key, defValue)
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
prefs.edit {
|
||||
putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInt(key: String?, defValue: Int): Int {
|
||||
return prefs.getInt(key, defValue)
|
||||
}
|
||||
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
prefs.edit {
|
||||
putInt(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(key: String?, defValue: Long): Long {
|
||||
return prefs.getLong(key, defValue)
|
||||
}
|
||||
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
prefs.edit {
|
||||
putLong(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFloat(key: String?, defValue: Float): Float {
|
||||
return prefs.getFloat(key, defValue)
|
||||
}
|
||||
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
prefs.edit {
|
||||
putFloat(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getString(key: String?, defValue: String?): String? {
|
||||
return prefs.getString(key, defValue)
|
||||
}
|
||||
|
||||
override fun putString(key: String?, value: String?) {
|
||||
prefs.edit {
|
||||
putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
|
||||
return prefs.getStringSet(key, defValues)
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
||||
prefs.edit {
|
||||
putStringSet(key, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.util.lang.use
|
||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
@@ -100,7 +101,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
||||
}
|
||||
|
||||
init {
|
||||
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
|
||||
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||
@@ -25,7 +26,8 @@ class AnimeExtensionInstallActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||
.setDataAndType(intent.data, intent.type)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
@@ -27,7 +28,7 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
context.registerReceiver(this, filter)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -216,7 +216,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
|
||||
isRegistered = true
|
||||
|
||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
context.registerReceiver(this, filter)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.util.lang.use
|
||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
@@ -100,7 +101,7 @@ class PackageInstallerInstallerManga(private val service: Service) : InstallerMa
|
||||
}
|
||||
|
||||
init {
|
||||
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
|
||||
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.others.LangSet
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||
@@ -25,7 +26,8 @@ class MangaExtensionInstallActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||
.setDataAndType(intent.data, intent.type)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
@@ -27,7 +28,7 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
context.registerReceiver(this, filter)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -213,7 +213,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||
isRegistered = true
|
||||
|
||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
context.registerReceiver(this, filter)
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ const val PREF_DOH_MULLVAD = 9
|
||||
const val PREF_DOH_CONTROLD = 10
|
||||
const val PREF_DOH_NJALLA = 11
|
||||
const val PREF_DOH_SHECAN = 12
|
||||
const val PREF_DOH_LIBREDNS = 13
|
||||
|
||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
@@ -184,3 +185,13 @@ fun OkHttpClient.Builder.dohShecan() = dns(
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohLibreDNS() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://doh.libredns.gr/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("116.202.176.26"), // IPv4 address for LibreDNS
|
||||
InetAddress.getByName("192.71.166.92") // Fallback IPv4 address
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -62,6 +62,7 @@ class NetworkHelper(
|
||||
PREF_DOH_CONTROLD -> builder.dohControlD()
|
||||
PREF_DOH_NJALLA -> builder.dohNajalla()
|
||||
PREF_DOH_SHECAN -> builder.dohShecan()
|
||||
PREF_DOH_LIBREDNS -> builder.dohLibreDNS()
|
||||
}
|
||||
|
||||
return builder
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package eu.kanade.tachiyomi.source.anime
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import tachiyomi.domain.source.anime.model.AnimeSourceData
|
||||
import tachiyomi.domain.source.anime.model.StubAnimeSource
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import tachiyomi.source.local.entries.anime.LocalAnimeSource
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class AndroidAnimeSourceManager(
|
||||
private val context: Context,
|
||||
private val extensionManager: AnimeExtensionManager,
|
||||
) : AnimeSourceManager {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, AnimeSource>())
|
||||
|
||||
private val stubSourcesMap = ConcurrentHashMap<Long, StubAnimeSource>()
|
||||
|
||||
override val catalogueSources: Flow<List<AnimeCatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<AnimeCatalogueSource>() }
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
extensionManager.installedExtensionsFlow
|
||||
.collectLatest { extensions ->
|
||||
val mutableMap = ConcurrentHashMap<Long, AnimeSource>(
|
||||
mapOf(
|
||||
LocalAnimeSource.ID to LocalAnimeSource(
|
||||
context,
|
||||
),
|
||||
),
|
||||
)
|
||||
extensions.forEach { extension ->
|
||||
extension.sources.forEach {
|
||||
mutableMap[it.id] = it
|
||||
registerStubSource(it.toSourceData())
|
||||
}
|
||||
}
|
||||
sourcesMapFlow.value = mutableMap
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun get(sourceKey: Long): AnimeSource? {
|
||||
return sourcesMapFlow.value[sourceKey]
|
||||
}
|
||||
|
||||
override fun getOrStub(sourceKey: Long): AnimeSource {
|
||||
return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
||||
runBlocking { createStubSource(sourceKey) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<AnimeHttpSource>()
|
||||
|
||||
override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance<AnimeCatalogueSource>()
|
||||
|
||||
override fun getStubSources(): List<StubAnimeSource> {
|
||||
val onlineSourceIds = getOnlineSources().map { it.id }
|
||||
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
|
||||
}
|
||||
|
||||
private fun registerStubSource(sourceData: AnimeSourceData) {
|
||||
|
||||
}
|
||||
|
||||
private suspend fun createStubSource(id: Long): StubAnimeSource {
|
||||
return StubAnimeSource(AnimeSourceData(id, "", ""))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package eu.kanade.tachiyomi.source.anime
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import tachiyomi.domain.source.anime.model.AnimeSourceData
|
||||
import tachiyomi.domain.source.anime.model.StubAnimeSource
|
||||
import tachiyomi.source.local.entries.anime.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this.id)
|
||||
|
||||
fun AnimeSource.getPreferenceKey(): String = "source_$id"
|
||||
|
||||
fun AnimeSource.toSourceData(): AnimeSourceData = AnimeSourceData(id = id, lang = lang, name = name)
|
||||
|
||||
fun AnimeSource.getNameForAnimeInfo(): String {
|
||||
val preferences = Injekt.get<SourcePreferences>()
|
||||
val enabledLanguages = preferences.enabledLanguages().get()
|
||||
.filterNot { it in listOf("all", "other") }
|
||||
val hasOneActiveLanguages = enabledLanguages.size == 1
|
||||
val isInEnabledLanguages = lang in enabledLanguages
|
||||
return when {
|
||||
// For edge cases where user disables a source they got manga of in their library.
|
||||
hasOneActiveLanguages && !isInEnabledLanguages -> toString()
|
||||
// Hide the language tag when only one language is used.
|
||||
hasOneActiveLanguages && isInEnabledLanguages -> name
|
||||
else -> toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun AnimeSource.isLocalOrStub(): Boolean = isLocal() || this is StubAnimeSource
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user