mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-28 16:21:02 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26f9f40042 | ||
|
|
7545870f38 | ||
|
|
3368a1bc8d | ||
|
|
9c0ef7a788 | ||
|
|
960c2b4113 | ||
|
|
1eb85d4419 | ||
|
|
20bea76e6c | ||
|
|
866bd3b3a9 | ||
|
|
3567b8dced | ||
|
|
d109914537 | ||
|
|
da4d55a9a8 | ||
|
|
63526c6ed3 |
16
README.md
16
README.md
@@ -1,6 +1,4 @@
|
|||||||
# **Dantotsu** (🚧 ALPHA 🚧)
|
# **Dantotsu**
|
||||||
|
|
||||||
> ⚠️ **WARNING**: This project is in alpha stage. Things may not work as expected.
|
|
||||||
|
|
||||||
<p align="center">
|
<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://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
|
||||||
@@ -26,14 +24,14 @@ Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-o
|
|||||||
| Type | Status |
|
| Type | Status |
|
||||||
| ---------------- | ------- |
|
| ---------------- | ------- |
|
||||||
| Anime Extensions | Working |
|
| Anime Extensions | Working |
|
||||||
| Manga Extensions | "Working" |
|
| Manga Extensions | Working |
|
||||||
| Light Novel Extensions | Not Working |
|
| Light Novel Extensions | Not Working |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## APP FEATURES
|
## APP FEATURES
|
||||||
|
|
||||||
- Easy and functional way to both, watch anime and read manga, ad-free.
|
- Easy and functional way to both, watch anime and read manga.
|
||||||
|
|
||||||
- A completely open source app with a nice UI & Animations :)
|
- A completely open source app with a nice UI & Animations :)
|
||||||
|
|
||||||
@@ -59,14 +57,16 @@ add your own extensions in the settings menu (Dantotsu has no affiliation with a
|
|||||||
|
|
||||||
## Planned Stuff
|
## Planned Stuff
|
||||||
|
|
||||||
- get app out of alpha
|
- TV Support
|
||||||
|
|
||||||
- Accent Color Change (RIP Hot Pink Supremacy.)
|
- LN Support
|
||||||
|
|
||||||
|
- Offline Viewing
|
||||||
|
|
||||||
|
|
||||||
## Rejected Stuff (still rejected)
|
## Rejected Stuff (still rejected)
|
||||||
|
|
||||||
- Sources of any language except English
|
- Official support of any language except English
|
||||||
|
|
||||||
- News Section in the App
|
- News Section in the App
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ android {
|
|||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||||
versionName "0.1.2"
|
versionName "1.0.0"
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||||
|
implementation 'androidx.preference:preference:1.2.1'
|
||||||
|
|
||||||
// Glide
|
// Glide
|
||||||
ext.glide_version = '4.16.0'
|
ext.glide_version = '4.16.0'
|
||||||
@@ -96,6 +97,7 @@ dependencies {
|
|||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||||
|
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||||
|
|
||||||
// string matching
|
// string matching
|
||||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Dantotsu α</string>
|
<string name="app_name">Dantotsu</string>
|
||||||
</resources>
|
</resources>
|
||||||
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 |
@@ -188,6 +188,9 @@ fun Activity.hideStatusBar() {
|
|||||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.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) {
|
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
package ani.dantotsu
|
package ani.dantotsu
|
||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnticipateInterpolator
|
import android.view.animation.AnticipateInterpolator
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
@@ -41,8 +50,10 @@ import ani.dantotsu.media.MediaDetailsActivity
|
|||||||
import ani.dantotsu.others.CustomBottomDialog
|
import ani.dantotsu.others.CustomBottomDialog
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.parsers.MangaSources
|
||||||
|
import ani.dantotsu.settings.SettingsActivity
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
@@ -53,6 +64,8 @@ import kotlinx.coroutines.flow.first
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@@ -63,15 +76,32 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private var load = false
|
private var load = false
|
||||||
|
|
||||||
private var uiSettings = UserInterfaceSettings()
|
private var uiSettings = UserInterfaceSettings()
|
||||||
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
|
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
|
||||||
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
|
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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)
|
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||||
animeScope.launch {
|
animeScope.launch {
|
||||||
animeExtensionManager.findAvailableExtensions()
|
animeExtensionManager.findAvailableExtensions()
|
||||||
@@ -101,24 +131,40 @@ class MainActivity : AppCompatActivity() {
|
|||||||
binding.root.isMotionEventSplittingEnabled = false
|
binding.root.isMotionEventSplittingEnabled = false
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val splash = SplashScreenBinding.inflate(layoutInflater)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
binding.root.addView(splash.root)
|
val splash = SplashScreenBinding.inflate(layoutInflater)
|
||||||
(splash.splashImage.drawable as Animatable).start()
|
binding.root.addView(splash.root)
|
||||||
|
(splash.splashImage.drawable as Animatable).start()
|
||||||
|
|
||||||
// Wait for 2 seconds (2000 milliseconds)
|
delay(1200)
|
||||||
delay(2000)
|
|
||||||
|
|
||||||
// Now perform the animation
|
ObjectAnimator.ofFloat(
|
||||||
ObjectAnimator.ofFloat(
|
splash.root,
|
||||||
splash.root,
|
View.TRANSLATION_Y,
|
||||||
View.TRANSLATION_Y,
|
0f,
|
||||||
0f,
|
-splash.root.height.toFloat()
|
||||||
-splash.root.height.toFloat()
|
).apply {
|
||||||
).apply {
|
interpolator = AnticipateInterpolator()
|
||||||
interpolator = AnticipateInterpolator()
|
duration = 200L
|
||||||
duration = 200L
|
doOnEnd { binding.root.removeView(splash.root) }
|
||||||
doOnEnd { binding.root.removeView(splash.root) }
|
start()
|
||||||
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +274,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//ViewPager
|
//ViewPager
|
||||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||||
@@ -244,4 +295,4 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package ani.dantotsu.aniyomi.anime.custom
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
@@ -12,7 +13,11 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
|||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
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 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.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
@@ -26,9 +31,11 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory { NetworkHelper(app, get()) }
|
addSingletonFactory { NetworkHelper(app, get()) }
|
||||||
|
|
||||||
addSingletonFactory { AnimeExtensionManager(app) }
|
addSingletonFactory { AnimeExtensionManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { MangaExtensionManager(app) }
|
addSingletonFactory { MangaExtensionManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||||
|
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||||
|
|
||||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
addSingleton(sharedPreferences)
|
addSingleton(sharedPreferences)
|
||||||
|
|
||||||
@@ -40,6 +47,11 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSingletonFactory { MangaCache() }
|
addSingletonFactory { MangaCache() }
|
||||||
|
|
||||||
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
|
get<AnimeSourceManager>()
|
||||||
|
get<MangaSourceManager>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ fun updateProgress(media: Media, number: String) {
|
|||||||
if (Anilist.userid != null) {
|
if (Anilist.userid != null) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val a = number.toFloatOrNull()?.roundToInt()
|
val a = number.toFloatOrNull()?.roundToInt()
|
||||||
if (a != media.userProgress) {
|
if ((a?:0) > (media.userProgress?:0)) {
|
||||||
Anilist.mutation.editList(
|
Anilist.mutation.editList(
|
||||||
media.id,
|
media.id,
|
||||||
a,
|
a,
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.logger
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
val data: Uri? = intent?.data
|
val data: Uri? = intent?.data
|
||||||
logger(data.toString())
|
logger(data.toString())
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import android.os.Bundle
|
|||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import ani.dantotsu.loadMedia
|
import ani.dantotsu.loadMedia
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class UrlMedia : Activity() {
|
class UrlMedia : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||||
var isMAL = false
|
var isMAL = false
|
||||||
var continueMedia = true
|
var continueMedia = true
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
val process = getProcessName()
|
val process = getProcessName()
|
||||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.connections.mal.MAL.clientId
|
import ani.dantotsu.connections.mal.MAL.clientId
|
||||||
import ani.dantotsu.connections.mal.MAL.saveResponse
|
import ani.dantotsu.connections.mal.MAL.saveResponse
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class Login : AppCompatActivity() {
|
class Login : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
try {
|
try {
|
||||||
val data: Uri = intent?.data
|
val data: Uri = intent?.data
|
||||||
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ object Helper {
|
|||||||
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
||||||
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
||||||
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
||||||
|
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -17,7 +20,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.media.GenreActivity
|
import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.MediaPageTransformer
|
import ani.dantotsu.MediaPageTransformer
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
@@ -31,6 +36,8 @@ import ani.dantotsu.setSlideUp
|
|||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
|
||||||
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
||||||
val ready = MutableLiveData(false)
|
val ready = MutableLiveData(false)
|
||||||
@@ -49,6 +56,23 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
binding = holder.binding
|
binding = holder.binding
|
||||||
trendingViewPager = binding.animeTrendingViewPager
|
trendingViewPager = binding.animeTrendingViewPager
|
||||||
|
|
||||||
|
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
||||||
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
|
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)
|
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -163,6 +187,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||||||
fun updateAvatar() {
|
fun updateAvatar() {
|
||||||
if (Anilist.avatar != null && ready.value == true) {
|
if (Anilist.avatar != null && ready.value == true) {
|
||||||
binding.animeUserAvatar.loadImage(Anilist.avatar)
|
binding.animeUserAvatar.loadImage(Anilist.avatar)
|
||||||
|
binding.animeUserAvatar.imageTintList = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package ani.dantotsu.home
|
package ani.dantotsu.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -17,7 +19,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.media.GenreActivity
|
import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.MediaPageTransformer
|
import ani.dantotsu.MediaPageTransformer
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
@@ -30,6 +34,8 @@ import ani.dantotsu.setSlideUp
|
|||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
|
||||||
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
||||||
val ready = MutableLiveData(false)
|
val ready = MutableLiveData(false)
|
||||||
@@ -48,6 +54,23 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
binding = holder.binding
|
binding = holder.binding
|
||||||
trendingViewPager = binding.mangaTrendingViewPager
|
trendingViewPager = binding.mangaTrendingViewPager
|
||||||
|
|
||||||
|
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
||||||
|
val currentColor = textInputLayout.boxBackgroundColor
|
||||||
|
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)
|
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -153,6 +176,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||||||
fun updateAvatar() {
|
fun updateAvatar() {
|
||||||
if (Anilist.avatar != null && ready.value == true) {
|
if (Anilist.avatar != null && ready.value == true) {
|
||||||
binding.mangaUserAvatar.loadImage(Anilist.avatar)
|
binding.mangaUserAvatar.loadImage(Anilist.avatar)
|
||||||
|
binding.mangaUserAvatar.imageTintList = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import ani.dantotsu.isOnline
|
|||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class NoInternet : AppCompatActivity() {
|
class NoInternet : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
val binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
val binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.ActivityAuthorBinding
|
import ani.dantotsu.databinding.ActivityAuthorBinding
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -28,6 +29,7 @@ class AuthorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package ani.dantotsu.media
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -11,7 +14,10 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityListBinding
|
import ani.dantotsu.databinding.ActivityListBinding
|
||||||
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||||
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,10 +33,40 @@ class CalendarActivity : AppCompatActivity() {
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityListBinding.inflate(layoutInflater)
|
binding = ActivityListBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, typedValue, true)
|
||||||
|
val primaryColor = typedValue.data
|
||||||
|
val typedValue2 = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true)
|
||||||
|
val primaryTextColor = typedValue2.data
|
||||||
|
val typedValue3 = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue3, true)
|
||||||
|
val secondaryColor = typedValue3.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.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||||
|
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
if (!uiSettings.immersiveMode) {
|
||||||
|
this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||||
|
binding.root.fitsSystemWindows = true
|
||||||
|
|
||||||
|
}else{
|
||||||
|
binding.root.fitsSystemWindows = false
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||||
|
}
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
|
||||||
binding.listTitle.setText(R.string.release_calendar)
|
binding.listTitle.setText(R.string.release_calendar)
|
||||||
binding.listSort.visibility = View.GONE
|
binding.listSort.visibility = View.GONE
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import ani.dantotsu.databinding.ActivityCharacterBinding
|
|||||||
import ani.dantotsu.others.ImageViewDialog
|
import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -33,6 +34,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ani.dantotsu.initActivity
|
|||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.loadData
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -25,6 +26,7 @@ class GenreActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityGenreBinding.inflate(layoutInflater)
|
binding = ActivityGenreBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import ani.dantotsu.saveData
|
|||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
@@ -71,6 +72,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||||
@@ -79,7 +81,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv)
|
if (!uiSettings.immersiveMode) this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||||
|
|
||||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||||
@@ -101,10 +104,14 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
if (uiSettings.bannerAnimations) {
|
if (uiSettings.bannerAnimations) {
|
||||||
val adi = AccelerateDecelerateInterpolator()
|
val adi = AccelerateDecelerateInterpolator()
|
||||||
val generator = RandomTransitionGenerator((10000 + 15000 * (uiSettings.animationSpeed)).toLong(), adi)
|
val generator = RandomTransitionGenerator(
|
||||||
|
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||||
|
adi
|
||||||
|
)
|
||||||
binding.mediaBanner.setTransitionGenerator(generator)
|
binding.mediaBanner.setTransitionGenerator(generator)
|
||||||
}
|
}
|
||||||
val banner = if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
val banner =
|
||||||
|
if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||||
val viewPager = binding.mediaViewPager
|
val viewPager = binding.mediaViewPager
|
||||||
tabLayout = binding.mediaTab as NavigationBarView
|
tabLayout = binding.mediaTab as NavigationBarView
|
||||||
viewPager.isUserInputEnabled = false
|
viewPager.isUserInputEnabled = false
|
||||||
@@ -162,13 +169,28 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
R.drawable.ic_round_favorite_24
|
R.drawable.ic_round_favorite_24
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
this.theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val color = typedValue.data
|
||||||
|
val typedValue2 = TypedValue()
|
||||||
|
this.theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
|
typedValue2,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val color2 = typedValue.data
|
||||||
|
|
||||||
PopImageButton(
|
PopImageButton(
|
||||||
scope,
|
scope,
|
||||||
binding.mediaFav,
|
binding.mediaFav,
|
||||||
R.drawable.ic_round_favorite_24,
|
R.drawable.ic_round_favorite_24,
|
||||||
R.drawable.ic_round_favorite_border_24,
|
R.drawable.ic_round_favorite_border_24,
|
||||||
R.color.nav_tab,
|
R.color.bg_opp,
|
||||||
R.color.fav,
|
R.color.violet_400,//TODO: Change to colorSecondary
|
||||||
media.isFav
|
media.isFav
|
||||||
) {
|
) {
|
||||||
media.isFav = it
|
media.isFav = it
|
||||||
@@ -180,17 +202,36 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ResourceType")
|
||||||
fun total() {
|
fun total() {
|
||||||
val text = SpannableStringBuilder().apply {
|
val text = SpannableStringBuilder().apply {
|
||||||
val white = ContextCompat.getColor(this@MediaDetailsActivity, R.color.bg_opp)
|
val typedValue = TypedValue()
|
||||||
|
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorOnBackground,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val white = typedValue.data
|
||||||
if (media.userStatus != null) {
|
if (media.userStatus != null) {
|
||||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue, true)
|
theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
bold { color(typedValue.data) { append("${media.userProgress}") } }
|
bold { color(typedValue.data) { append("${media.userProgress}") } }
|
||||||
append(if (media.anime != null) getString(R.string.episodes_out_of) else getString(R.string.chapters_out_of))
|
append(
|
||||||
|
if (media.anime != null) getString(R.string.episodes_out_of) else getString(
|
||||||
|
R.string.chapters_out_of
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
append(if (media.anime != null) getString(R.string.episodes_total_of) else getString(R.string.chapters_total_of))
|
append(
|
||||||
|
if (media.anime != null) getString(R.string.episodes_total_of) else getString(
|
||||||
|
R.string.chapters_total_of
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
if (media.anime!!.nextAiringEpisode != null) {
|
if (media.anime!!.nextAiringEpisode != null) {
|
||||||
@@ -206,8 +247,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
fun progress() {
|
fun progress() {
|
||||||
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
val statuses: Array<String> = resources.getStringArray(R.array.status)
|
||||||
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
|
val statusStrings =
|
||||||
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
|
||||||
|
R.array.status_manga
|
||||||
|
)
|
||||||
|
val userStatus =
|
||||||
|
if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
|
||||||
|
|
||||||
if (media.userStatus != null) {
|
if (media.userStatus != null) {
|
||||||
binding.mediaTotal.visibility = View.VISIBLE
|
binding.mediaTotal.visibility = View.VISIBLE
|
||||||
@@ -234,7 +279,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
if (it != null) {
|
if (it != null) {
|
||||||
media = it
|
media = it
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if(media.isFav!=favButton?.clicked) favButton?.clicked()
|
if (media.isFav != favButton?.clicked) favButton?.clicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mediaNotify.setOnClickListener {
|
binding.mediaNotify.setOnClickListener {
|
||||||
@@ -258,10 +303,15 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
tabLayout.menu.clear()
|
tabLayout.menu.clear()
|
||||||
if (media.anime != null) {
|
if (media.anime != null) {
|
||||||
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
|
viewPager.adapter =
|
||||||
|
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
|
||||||
tabLayout.inflateMenu(R.menu.anime_menu_detail)
|
tabLayout.inflateMenu(R.menu.anime_menu_detail)
|
||||||
} else if (media.manga != null) {
|
} else if (media.manga != null) {
|
||||||
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, if(media.format=="NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA)
|
viewPager.adapter = ViewPagerAdapter(
|
||||||
|
supportFragmentManager,
|
||||||
|
lifecycle,
|
||||||
|
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
|
||||||
|
)
|
||||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||||
anime = false
|
anime = false
|
||||||
}
|
}
|
||||||
@@ -303,9 +353,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
private fun selectFromID(id: Int) {
|
private fun selectFromID(id: Int) {
|
||||||
when (id) {
|
when (id) {
|
||||||
R.id.info -> {
|
R.id.info -> {
|
||||||
selected = 0
|
selected = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.watch, R.id.read -> {
|
R.id.watch, R.id.read -> {
|
||||||
selected = 1
|
selected = 1
|
||||||
}
|
}
|
||||||
@@ -329,9 +380,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class SupportedMedia{
|
private enum class SupportedMedia {
|
||||||
ANIME, MANGA, NOVEL
|
ANIME, MANGA, NOVEL
|
||||||
}
|
}
|
||||||
|
|
||||||
//ViewPager
|
//ViewPager
|
||||||
private class ViewPagerAdapter(
|
private class ViewPagerAdapter(
|
||||||
fragmentManager: FragmentManager,
|
fragmentManager: FragmentManager,
|
||||||
@@ -342,13 +394,14 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
|
|
||||||
override fun getItemCount(): Int = 2
|
override fun getItemCount(): Int = 2
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = when (position){
|
override fun createFragment(position: Int): Fragment = when (position) {
|
||||||
0 -> MediaInfoFragment()
|
0 -> MediaInfoFragment()
|
||||||
1 -> when(media){
|
1 -> when (media) {
|
||||||
SupportedMedia.ANIME -> AnimeWatchFragment()
|
SupportedMedia.ANIME -> AnimeWatchFragment()
|
||||||
SupportedMedia.MANGA -> MangaReadFragment()
|
SupportedMedia.MANGA -> MangaReadFragment()
|
||||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> MediaInfoFragment()
|
else -> MediaInfoFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,27 +416,45 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
|
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
|
||||||
val percentage = abs(i) * 100 / mMaxScrollSize
|
val percentage = abs(i) * 100 / mMaxScrollSize
|
||||||
|
|
||||||
binding.mediaCover.visibility = if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
binding.mediaCover.visibility =
|
||||||
|
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||||
val duration = (200 * uiSettings.animationSpeed).toLong()
|
val duration = (200 * uiSettings.animationSpeed).toLong()
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||||
|
com.google.android.material.R.attr.colorSecondary,
|
||||||
|
typedValue,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val color = typedValue.data
|
||||||
if (percentage >= percent && !isCollapsed) {
|
if (percentage >= percent && !isCollapsed) {
|
||||||
isCollapsed = true
|
isCollapsed = true
|
||||||
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration).start()
|
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
|
||||||
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", screenWidth).setDuration(duration).start()
|
.start()
|
||||||
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", screenWidth).setDuration(duration).start()
|
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", screenWidth)
|
||||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth).setDuration(duration).start()
|
.setDuration(duration).start()
|
||||||
|
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", screenWidth)
|
||||||
|
.setDuration(duration).start()
|
||||||
|
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
|
||||||
|
.setDuration(duration).start()
|
||||||
binding.mediaBanner.pause()
|
binding.mediaBanner.pause()
|
||||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
||||||
}
|
}
|
||||||
if (percentage <= percent && isCollapsed) {
|
if (percentage <= percent && isCollapsed) {
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", -screenWidth).setDuration(duration).start()
|
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", -screenWidth)
|
||||||
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", 0f).setDuration(duration).start()
|
.setDuration(duration).start()
|
||||||
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", 0f).setDuration(duration).start()
|
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", 0f)
|
||||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f).setDuration(duration).start()
|
.setDuration(duration).start()
|
||||||
|
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", 0f).setDuration(duration)
|
||||||
|
.start()
|
||||||
|
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
|
||||||
|
.setDuration(duration).start()
|
||||||
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
||||||
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv)
|
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
|
||||||
}
|
}
|
||||||
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(false)
|
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
|
||||||
|
false
|
||||||
|
)
|
||||||
if (percentage == 0 && model.scrolledToTop.value != true) model.scrolledToTop.postValue(true)
|
if (percentage == 0 && model.scrolledToTop.value != true) model.scrolledToTop.postValue(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,8 +496,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
ObjectAnimator.ofFloat(image, "scaleX", 1f, 0f).setDuration(69).start()
|
ObjectAnimator.ofFloat(image, "scaleX", 1f, 0f).setDuration(69).start()
|
||||||
ObjectAnimator.ofFloat(image, "scaleY", 1f, 0f).setDuration(100).start()
|
ObjectAnimator.ofFloat(image, "scaleY", 1f, 0f).setDuration(100).start()
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
||||||
if (clicked) {
|
if (clicked) {
|
||||||
ObjectAnimator.ofArgb(image,
|
ObjectAnimator.ofArgb(
|
||||||
|
image,
|
||||||
"ColorFilter",
|
"ColorFilter",
|
||||||
ContextCompat.getColor(context, c1),
|
ContextCompat.getColor(context, c1),
|
||||||
ContextCompat.getColor(context, c2)
|
ContextCompat.getColor(context, c2)
|
||||||
@@ -439,17 +512,19 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||||||
ObjectAnimator.ofFloat(image, "scaleX", 1.5f, 1f).setDuration(100).start()
|
ObjectAnimator.ofFloat(image, "scaleX", 1.5f, 1f).setDuration(100).start()
|
||||||
ObjectAnimator.ofFloat(image, "scaleY", 1.5f, 1f).setDuration(100).start()
|
ObjectAnimator.ofFloat(image, "scaleY", 1.5f, 1f).setDuration(100).start()
|
||||||
delay(200)
|
delay(200)
|
||||||
if (clicked) ObjectAnimator.ofArgb(
|
if (clicked) {
|
||||||
image,
|
ObjectAnimator.ofArgb(
|
||||||
"ColorFilter",
|
image,
|
||||||
ContextCompat.getColor(context, c2),
|
"ColorFilter",
|
||||||
ContextCompat.getColor(context, c1)
|
ContextCompat.getColor(context, c2),
|
||||||
).setDuration(200).start()
|
ContextCompat.getColor(context, c1)
|
||||||
|
).setDuration(200).start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enabled(enabled: Boolean) {
|
fun enabled(enabled: Boolean) {
|
||||||
disabled = !enabled
|
disabled = !enabled
|
||||||
image.alpha = if(disabled) 0.33f else 1f
|
image.alpha = if (disabled) 0.33f else 1f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
|
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
|
||||||
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
|
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
|
||||||
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
|
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
|
||||||
suspend fun loadEpisodes(media: Media, i: Int) {
|
suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) {
|
||||||
if (!epsLoaded.containsKey(i)) {
|
if (!epsLoaded.containsKey(i) || invalidate) {
|
||||||
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
|
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
|
||||||
}
|
}
|
||||||
episodes.postValue(epsLoaded)
|
episodes.postValue(epsLoaded)
|
||||||
@@ -240,9 +240,9 @@ class MediaDetailsViewModel : ViewModel() {
|
|||||||
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
|
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
|
||||||
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
|
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
|
||||||
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters
|
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")
|
logger("Loading Manga Chapters : $mangaLoaded")
|
||||||
if (!mangaLoaded.containsKey(i)) tryWithSuspend {
|
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
||||||
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||||
}
|
}
|
||||||
mangaChapters.postValue(mangaLoaded)
|
mangaChapters.postValue(mangaLoaded)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||||
import ani.dantotsu.connections.anilist.SearchResults
|
import ani.dantotsu.connections.anilist.SearchResults
|
||||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -37,6 +38,7 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivitySearchBinding.inflate(layoutInflater)
|
binding = ActivitySearchBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
initActivity(this)
|
initActivity(this)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data class Selected(
|
|||||||
var chip: Int = 0,
|
var chip: Int = 0,
|
||||||
//var source: String = "",
|
//var source: String = "",
|
||||||
var sourceIndex: Int = 0,
|
var sourceIndex: Int = 0,
|
||||||
|
var langIndex: Int = 0,
|
||||||
var preferDub: Boolean = false,
|
var preferDub: Boolean = false,
|
||||||
var server: String? = null,
|
var server: String? = null,
|
||||||
var video: Int = 0,
|
var video: Int = 0,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.ActivityStudioBinding
|
import ani.dantotsu.databinding.ActivityStudioBinding
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -28,6 +29,7 @@ class StudioActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityStudioBinding.inflate(layoutInflater)
|
binding = ActivityStudioBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
48
app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
Normal file
48
app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Environment
|
||||||
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import okhttp3.Request
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
|
class SubtitleDownloader {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
//doesn't really download the subtitles -\_(o_o)_/-
|
||||||
|
suspend fun downloadSubtitles(context: Context, url: String): SubtitleType =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||||
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = networkHelper.client.newCall(request).execute()
|
||||||
|
|
||||||
|
// Check if response is successful
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val responseBody = response.body?.string()
|
||||||
|
|
||||||
|
|
||||||
|
val subtitleType = when {
|
||||||
|
responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS
|
||||||
|
responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT
|
||||||
|
else -> SubtitleType.SRT
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext subtitleType
|
||||||
|
} else {
|
||||||
|
return@withContext SubtitleType.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,47 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||||
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
|
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||||
import ani.dantotsu.parsers.WatchSources
|
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.Notifications.Companion.openSettings
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import com.google.android.material.chip.Chip
|
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.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.lang.IndexOutOfBoundsException
|
||||||
|
|
||||||
class AnimeWatchAdapter(
|
class AnimeWatchAdapter(
|
||||||
private val media: Media,
|
private val media: Media,
|
||||||
@@ -68,7 +87,8 @@ class AnimeWatchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Source Selection
|
//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) {
|
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
|
||||||
binding.animeSource.setText(watchSources.names[source])
|
binding.animeSource.setText(watchSources.names[source])
|
||||||
watchSources[source].apply {
|
watchSources[source].apply {
|
||||||
@@ -90,11 +110,41 @@ class AnimeWatchAdapter(
|
|||||||
binding.animeSourceDubbed.isChecked = selectDub
|
binding.animeSourceDubbed.isChecked = selectDub
|
||||||
changing = false
|
changing = false
|
||||||
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
|
||||||
|
source = i
|
||||||
|
setLanguageList(0,i)
|
||||||
}
|
}
|
||||||
subscribeButton(false)
|
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
|
//Subscription
|
||||||
subscribe = MediaDetailsActivity.PopImageButton(
|
subscribe = MediaDetailsActivity.PopImageButton(
|
||||||
fragment.lifecycleScope,
|
fragment.lifecycleScope,
|
||||||
@@ -176,6 +226,7 @@ class AnimeWatchAdapter(
|
|||||||
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
||||||
}
|
}
|
||||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||||
|
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
|
||||||
|
|
||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
selected()
|
selected()
|
||||||
@@ -260,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
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package ani.dantotsu.media.anime
|
package ani.dantotsu.media.anime
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.math.MathUtils
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@@ -13,24 +19,38 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.parsers.AnimeParser
|
import ani.dantotsu.parsers.AnimeParser
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.HAnimeSources
|
import ani.dantotsu.parsers.HAnimeSources
|
||||||
|
import ani.dantotsu.settings.ExtensionsActivity
|
||||||
|
import ani.dantotsu.settings.InstalledAnimeExtensionsFragment
|
||||||
import ani.dantotsu.settings.PlayerSettings
|
import ani.dantotsu.settings.PlayerSettings
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||||
import ani.dantotsu.subcriptions.Notifications
|
import ani.dantotsu.subcriptions.Notifications
|
||||||
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -214,6 +234,13 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
return model.watchSources?.get(i)!!
|
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) {
|
fun onDubClicked(checked: Boolean) {
|
||||||
val selected = model.loadSelected(media)
|
val selected = model.loadSelected(media)
|
||||||
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
|
||||||
@@ -223,8 +250,8 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
|
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadEpisodes(i: Int) {
|
fun loadEpisodes(i: Int, invalidate: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) }
|
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onIconPressed(viewType: Int, rev: Boolean) {
|
fun onIconPressed(viewType: Int, rev: Boolean) {
|
||||||
@@ -262,45 +289,115 @@ class AnimeWatchFragment : Fragment() {
|
|||||||
else getString(R.string.unsubscribed_notification)
|
else getString(R.string.unsubscribed_notification)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fun openSettings(pkg: AnimeExtension.Installed){
|
||||||
fun onEpisodeClick(i: String) {
|
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||||
model.continueMedia = false
|
val activity = requireActivity() as MediaDetailsActivity
|
||||||
model.saveSelected(media.id, media.selected!!, requireActivity())
|
val visibility = if (show) View.VISIBLE else View.GONE
|
||||||
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
|
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||||
}
|
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||||
|
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||||
private fun reload() {
|
try{
|
||||||
val selected = model.loadSelected(media)
|
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||||
|
}catch (e: ClassCastException){
|
||||||
//Find latest episode for subscription
|
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||||
selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
}
|
||||||
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||||
|
if (show) View.GONE else View.VISIBLE
|
||||||
model.saveSelected(media.id, selected, requireActivity())
|
}
|
||||||
headerAdapter.handleEpisodes()
|
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||||
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
|
if (allSettings.isNotEmpty()) {
|
||||||
var arr: ArrayList<Episode> = arrayListOf()
|
var selectedSetting = allSettings[0]
|
||||||
if (media.anime!!.episodes != null) {
|
if (allSettings.size > 1) {
|
||||||
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
|
val names = allSettings.map { it.lang }.toTypedArray()
|
||||||
arr.addAll(
|
var selectedIndex = 0
|
||||||
media.anime!!.episodes!!.values.toList()
|
AlertDialog.Builder(requireContext())
|
||||||
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
|
.setTitle("Select a Source")
|
||||||
)
|
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||||
if (reverse)
|
selectedIndex = which
|
||||||
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
|
}
|
||||||
|
.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() {
|
fun onEpisodeClick(i: String) {
|
||||||
model.watchSources?.flushText()
|
model.continueMedia = false
|
||||||
super.onDestroy()
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
binding.mediaInfoProgressBar.visibility = progress
|
binding.mediaInfoProgressBar.visibility = progress
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
|
|||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
|
import androidx.media3.exoplayer.util.EventLogger
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.ui.*
|
import androidx.media3.ui.*
|
||||||
import androidx.media3.ui.CaptionStyleCompat.*
|
import androidx.media3.ui.CaptionStyleCompat.*
|
||||||
@@ -65,6 +66,7 @@ import ani.dantotsu.connections.updateProgress
|
|||||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.media.SubtitleDownloader
|
||||||
import ani.dantotsu.others.AniSkip
|
import ani.dantotsu.others.AniSkip
|
||||||
import ani.dantotsu.others.AniSkip.getType
|
import ani.dantotsu.others.AniSkip.getType
|
||||||
import ani.dantotsu.others.Download.download
|
import ani.dantotsu.others.Download.download
|
||||||
@@ -74,6 +76,7 @@ import ani.dantotsu.parsers.*
|
|||||||
import ani.dantotsu.settings.PlayerSettings
|
import ani.dantotsu.settings.PlayerSettings
|
||||||
import ani.dantotsu.settings.PlayerSettingsActivity
|
import ani.dantotsu.settings.PlayerSettingsActivity
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import com.lagradost.nicehttp.ignoreAllSSLErrors
|
import com.lagradost.nicehttp.ignoreAllSSLErrors
|
||||||
@@ -81,12 +84,15 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.internal.immutableListOf
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||||
class ExoplayerView : AppCompatActivity(), Player.Listener {
|
class ExoplayerView : AppCompatActivity(), Player.Listener {
|
||||||
@@ -183,7 +189,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
||||||
if (displayCutout != null) {
|
if (displayCutout != null) {
|
||||||
if (displayCutout.boundingRects.size > 0) {
|
if (displayCutout.boundingRects.size > 0) {
|
||||||
notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height())
|
notchHeight = min(
|
||||||
|
displayCutout.boundingRects[0].width(),
|
||||||
|
displayCutout.boundingRects[0].height()
|
||||||
|
)
|
||||||
checkNotch()
|
checkNotch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,101 +203,104 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
private fun checkNotch() {
|
private fun checkNotch() {
|
||||||
if (notchHeight != 0) {
|
if (notchHeight != 0) {
|
||||||
val orientation = resources.configuration.orientation
|
val orientation = resources.configuration.orientation
|
||||||
playerView.findViewById<View>(R.id.exo_controller_margin).updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
playerView.findViewById<View>(R.id.exo_controller_margin)
|
||||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
marginStart = notchHeight
|
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
marginEnd = notchHeight
|
marginStart = notchHeight
|
||||||
topMargin = 0
|
marginEnd = notchHeight
|
||||||
} else {
|
topMargin = 0
|
||||||
topMargin = notchHeight
|
} else {
|
||||||
marginStart = 0
|
topMargin = notchHeight
|
||||||
marginEnd = 0
|
marginStart = 0
|
||||||
|
marginEnd = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
playerView.findViewById<View>(androidx.media3.ui.R.id.exo_buffering).translationY =
|
playerView.findViewById<View>(androidx.media3.ui.R.id.exo_buffering).translationY =
|
||||||
(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8f.px)).dp
|
(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8f.px)).dp
|
||||||
exoBrightnessCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
exoBrightnessCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
marginEnd = if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
|
marginEnd =
|
||||||
|
if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
|
||||||
}
|
}
|
||||||
exoVolumeCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
exoVolumeCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
marginStart = if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
|
marginStart =
|
||||||
|
if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSubFormatting(playerView: PlayerView, settings: PlayerSettings) {
|
private fun setupSubFormatting(playerView: PlayerView, settings: PlayerSettings) {
|
||||||
val primaryColor = when (settings.primaryColor) {
|
val primaryColor = when (settings.primaryColor) {
|
||||||
0 -> Color.BLACK
|
0 -> Color.BLACK
|
||||||
1 -> Color.DKGRAY
|
1 -> Color.DKGRAY
|
||||||
2 -> Color.GRAY
|
2 -> Color.GRAY
|
||||||
3 -> Color.LTGRAY
|
3 -> Color.LTGRAY
|
||||||
4 -> Color.WHITE
|
4 -> Color.WHITE
|
||||||
5 -> Color.RED
|
5 -> Color.RED
|
||||||
6 -> Color.YELLOW
|
6 -> Color.YELLOW
|
||||||
7 -> Color.GREEN
|
7 -> Color.GREEN
|
||||||
8 -> Color.CYAN
|
8 -> Color.CYAN
|
||||||
9 -> Color.BLUE
|
9 -> Color.BLUE
|
||||||
10 -> Color.MAGENTA
|
10 -> Color.MAGENTA
|
||||||
11 -> Color.TRANSPARENT
|
11 -> Color.TRANSPARENT
|
||||||
else -> Color.WHITE
|
else -> Color.WHITE
|
||||||
}
|
}
|
||||||
val secondaryColor = when (settings.secondaryColor) {
|
val secondaryColor = when (settings.secondaryColor) {
|
||||||
0 -> Color.BLACK
|
0 -> Color.BLACK
|
||||||
1 -> Color.DKGRAY
|
1 -> Color.DKGRAY
|
||||||
2 -> Color.GRAY
|
2 -> Color.GRAY
|
||||||
3 -> Color.LTGRAY
|
3 -> Color.LTGRAY
|
||||||
4 -> Color.WHITE
|
4 -> Color.WHITE
|
||||||
5 -> Color.RED
|
5 -> Color.RED
|
||||||
6 -> Color.YELLOW
|
6 -> Color.YELLOW
|
||||||
7 -> Color.GREEN
|
7 -> Color.GREEN
|
||||||
8 -> Color.CYAN
|
8 -> Color.CYAN
|
||||||
9 -> Color.BLUE
|
9 -> Color.BLUE
|
||||||
10 -> Color.MAGENTA
|
10 -> Color.MAGENTA
|
||||||
11 -> Color.TRANSPARENT
|
11 -> Color.TRANSPARENT
|
||||||
else -> Color.BLACK
|
else -> Color.BLACK
|
||||||
}
|
}
|
||||||
val outline = when (settings.outline) {
|
val outline = when (settings.outline) {
|
||||||
0 -> EDGE_TYPE_OUTLINE // Normal
|
0 -> EDGE_TYPE_OUTLINE // Normal
|
||||||
1 -> EDGE_TYPE_DEPRESSED // Shine
|
1 -> EDGE_TYPE_DEPRESSED // Shine
|
||||||
2 -> EDGE_TYPE_DROP_SHADOW // Drop shadow
|
2 -> EDGE_TYPE_DROP_SHADOW // Drop shadow
|
||||||
3 -> EDGE_TYPE_NONE // No outline
|
3 -> EDGE_TYPE_NONE // No outline
|
||||||
else -> EDGE_TYPE_OUTLINE // Normal
|
else -> EDGE_TYPE_OUTLINE // Normal
|
||||||
}
|
}
|
||||||
val subBackground = when (settings.subBackground) {
|
val subBackground = when (settings.subBackground) {
|
||||||
0 -> Color.TRANSPARENT
|
0 -> Color.TRANSPARENT
|
||||||
1 -> Color.BLACK
|
1 -> Color.BLACK
|
||||||
2 -> Color.DKGRAY
|
2 -> Color.DKGRAY
|
||||||
3 -> Color.GRAY
|
3 -> Color.GRAY
|
||||||
4 -> Color.LTGRAY
|
4 -> Color.LTGRAY
|
||||||
5 -> Color.WHITE
|
5 -> Color.WHITE
|
||||||
6 -> Color.RED
|
6 -> Color.RED
|
||||||
7 -> Color.YELLOW
|
7 -> Color.YELLOW
|
||||||
8 -> Color.GREEN
|
8 -> Color.GREEN
|
||||||
9 -> Color.CYAN
|
9 -> Color.CYAN
|
||||||
10 -> Color.BLUE
|
10 -> Color.BLUE
|
||||||
11 -> Color.MAGENTA
|
11 -> Color.MAGENTA
|
||||||
else -> Color.TRANSPARENT
|
else -> Color.TRANSPARENT
|
||||||
}
|
}
|
||||||
val subWindow = when (settings.subWindow) {
|
val subWindow = when (settings.subWindow) {
|
||||||
0 -> Color.TRANSPARENT
|
0 -> Color.TRANSPARENT
|
||||||
1 -> Color.BLACK
|
1 -> Color.BLACK
|
||||||
2 -> Color.DKGRAY
|
2 -> Color.DKGRAY
|
||||||
3 -> Color.GRAY
|
3 -> Color.GRAY
|
||||||
4 -> Color.LTGRAY
|
4 -> Color.LTGRAY
|
||||||
5 -> Color.WHITE
|
5 -> Color.WHITE
|
||||||
6 -> Color.RED
|
6 -> Color.RED
|
||||||
7 -> Color.YELLOW
|
7 -> Color.YELLOW
|
||||||
8 -> Color.GREEN
|
8 -> Color.GREEN
|
||||||
9 -> Color.CYAN
|
9 -> Color.CYAN
|
||||||
10 -> Color.BLUE
|
10 -> Color.BLUE
|
||||||
11 -> Color.MAGENTA
|
11 -> Color.MAGENTA
|
||||||
else -> Color.TRANSPARENT
|
else -> Color.TRANSPARENT
|
||||||
}
|
}
|
||||||
val font = when (settings.font) {
|
val font = when (settings.font) {
|
||||||
0 -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
|
0 -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
|
||||||
1 -> ResourcesCompat.getFont(this, R.font.poppins_bold)
|
1 -> ResourcesCompat.getFont(this, R.font.poppins_bold)
|
||||||
2 -> ResourcesCompat.getFont(this, R.font.poppins)
|
2 -> ResourcesCompat.getFont(this, R.font.poppins)
|
||||||
3 -> ResourcesCompat.getFont(this, R.font.poppins_thin)
|
3 -> ResourcesCompat.getFont(this, R.font.poppins_thin)
|
||||||
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
|
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
|
||||||
}
|
}
|
||||||
playerView.subtitleView?.setStyle(
|
playerView.subtitleView?.setStyle(
|
||||||
@@ -305,6 +317,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityExoplayerBinding.inflate(layoutInflater)
|
binding = ActivityExoplayerBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
@@ -316,8 +329,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
settings = loadData("player_settings") ?: PlayerSettings().apply { saveData("player_settings", this) }
|
settings = loadData("player_settings") ?: PlayerSettings().apply {
|
||||||
uiSettings = loadData("ui_settings") ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
|
saveData(
|
||||||
|
"player_settings",
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiSettings = loadData("ui_settings") ?: UserInterfaceSettings().apply {
|
||||||
|
saveData(
|
||||||
|
"ui_settings",
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
playerView = findViewById(R.id.player_view)
|
playerView = findViewById(R.id.player_view)
|
||||||
exoQuality = playerView.findViewById(R.id.exo_quality)
|
exoQuality = playerView.findViewById(R.id.exo_quality)
|
||||||
@@ -360,17 +383,20 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
requestedOrientation = rotation
|
requestedOrientation = rotation
|
||||||
it.visibility = View.GONE
|
it.visibility = View.GONE
|
||||||
}
|
}
|
||||||
orientationListener = object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
orientationListener =
|
||||||
override fun onOrientationChanged(orientation: Int) {
|
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||||
if (orientation in 45..135) {
|
override fun onOrientationChanged(orientation: Int) {
|
||||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility = View.VISIBLE
|
if (orientation in 45..135) {
|
||||||
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility =
|
||||||
} else if (orientation in 225..315) {
|
View.VISIBLE
|
||||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) exoRotate.visibility = View.VISIBLE
|
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
} else if (orientation in 225..315) {
|
||||||
|
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) exoRotate.visibility =
|
||||||
|
View.VISIBLE
|
||||||
|
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
orientationListener?.enable()
|
orientationListener?.enable()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +404,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
|
|
||||||
|
|
||||||
playerView.subtitleView?.alpha = when (settings.subtitles) {
|
playerView.subtitleView?.alpha = when (settings.subtitles) {
|
||||||
true -> 1f
|
true -> 1f
|
||||||
false -> 0f
|
false -> 0f
|
||||||
}
|
}
|
||||||
val fontSize = settings.fontSize.toFloat()
|
val fontSize = settings.fontSize.toFloat()
|
||||||
@@ -401,7 +427,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
isTimeStampsLoaded = true
|
isTimeStampsLoaded = true
|
||||||
exoSkipOpEd.visibility = if (it != null) {
|
exoSkipOpEd.visibility = if (it != null) {
|
||||||
val adGroups = it.flatMap {
|
val adGroups = it.flatMap {
|
||||||
listOf(it.interval.startTime.toLong() * 1000, it.interval.endTime.toLong() * 1000)
|
listOf(
|
||||||
|
it.interval.startTime.toLong() * 1000,
|
||||||
|
it.interval.endTime.toLong() * 1000
|
||||||
|
)
|
||||||
}.toLongArray()
|
}.toLongArray()
|
||||||
val playedAdGroups = it.flatMap {
|
val playedAdGroups = it.flatMap {
|
||||||
listOf(false, false)
|
listOf(false, false)
|
||||||
@@ -441,7 +470,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
|
|
||||||
// Picture-in-picture
|
// Picture-in-picture
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
pipEnabled = packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && settings.pip
|
pipEnabled =
|
||||||
|
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && settings.pip
|
||||||
if (pipEnabled) {
|
if (pipEnabled) {
|
||||||
exoPip.visibility = View.VISIBLE
|
exoPip.visibility = View.VISIBLE
|
||||||
exoPip.setOnClickListener {
|
exoPip.setOnClickListener {
|
||||||
@@ -456,7 +486,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
val container = playerView.findViewById<View>(R.id.exo_controller_cont)
|
val container = playerView.findViewById<View>(R.id.exo_controller_cont)
|
||||||
val screen = playerView.findViewById<View>(R.id.exo_black_screen)
|
val screen = playerView.findViewById<View>(R.id.exo_black_screen)
|
||||||
val lockButton = playerView.findViewById<ImageButton>(R.id.exo_unlock)
|
val lockButton = playerView.findViewById<ImageButton>(R.id.exo_unlock)
|
||||||
val timeline = playerView.findViewById<ExtendedTimeBar>(androidx.media3.ui.R.id.exo_progress)
|
val timeline =
|
||||||
|
playerView.findViewById<ExtendedTimeBar>(androidx.media3.ui.R.id.exo_progress)
|
||||||
playerView.findViewById<ImageButton>(R.id.exo_lock).setOnClickListener {
|
playerView.findViewById<ImageButton>(R.id.exo_lock).setOnClickListener {
|
||||||
locked = true
|
locked = true
|
||||||
screen.visibility = View.GONE
|
screen.visibility = View.GONE
|
||||||
@@ -496,17 +527,22 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
dialog.findViewById<Slider>(R.id.seekbar).addOnChangeListener { _, value, _ ->
|
dialog.findViewById<Slider>(R.id.seekbar).addOnChangeListener { _, value, _ ->
|
||||||
settings.skipTime = value.toInt()
|
settings.skipTime = value.toInt()
|
||||||
saveData(player, settings)
|
saveData(player, settings)
|
||||||
playerView.findViewById<TextView>(R.id.exo_skip_time).text = settings.skipTime.toString()
|
playerView.findViewById<TextView>(R.id.exo_skip_time).text =
|
||||||
dialog.findViewById<TextView>(R.id.seekbar_value).text = settings.skipTime.toString()
|
settings.skipTime.toString()
|
||||||
|
dialog.findViewById<TextView>(R.id.seekbar_value).text =
|
||||||
|
settings.skipTime.toString()
|
||||||
}
|
}
|
||||||
dialog.findViewById<Slider>(R.id.seekbar).addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
dialog.findViewById<Slider>(R.id.seekbar)
|
||||||
override fun onStartTrackingTouch(slider: Slider) {}
|
.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
override fun onStartTrackingTouch(slider: Slider) {}
|
||||||
dialog.dismiss()
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
}
|
dialog.dismiss()
|
||||||
})
|
}
|
||||||
dialog.findViewById<TextView>(R.id.seekbar_title).text = getString(R.string.skip_time)
|
})
|
||||||
dialog.findViewById<TextView>(R.id.seekbar_value).text = settings.skipTime.toString()
|
dialog.findViewById<TextView>(R.id.seekbar_title).text =
|
||||||
|
getString(R.string.skip_time)
|
||||||
|
dialog.findViewById<TextView>(R.id.seekbar_value).text =
|
||||||
|
settings.skipTime.toString()
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
dialog.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
dialog.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
dialog.show()
|
dialog.show()
|
||||||
@@ -521,7 +557,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
val brightnessRunnable = Runnable {
|
val brightnessRunnable = Runnable {
|
||||||
if (exoBrightnessCont.alpha == 1f)
|
if (exoBrightnessCont.alpha == 1f)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
ObjectAnimator.ofFloat(exoBrightnessCont, "alpha", 1f, 0f).setDuration(gestureSpeed).start()
|
ObjectAnimator.ofFloat(exoBrightnessCont, "alpha", 1f, 0f)
|
||||||
|
.setDuration(gestureSpeed).start()
|
||||||
delay(gestureSpeed)
|
delay(gestureSpeed)
|
||||||
exoBrightnessCont.visibility = View.GONE
|
exoBrightnessCont.visibility = View.GONE
|
||||||
checkNotch()
|
checkNotch()
|
||||||
@@ -530,7 +567,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
val volumeRunnable = Runnable {
|
val volumeRunnable = Runnable {
|
||||||
if (exoVolumeCont.alpha == 1f)
|
if (exoVolumeCont.alpha == 1f)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
ObjectAnimator.ofFloat(exoVolumeCont, "alpha", 1f, 0f).setDuration(gestureSpeed).start()
|
ObjectAnimator.ofFloat(exoVolumeCont, "alpha", 1f, 0f).setDuration(gestureSpeed)
|
||||||
|
.start()
|
||||||
delay(gestureSpeed)
|
delay(gestureSpeed)
|
||||||
exoVolumeCont.visibility = View.GONE
|
exoVolumeCont.visibility = View.GONE
|
||||||
checkNotch()
|
checkNotch()
|
||||||
@@ -548,25 +586,65 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
fun handleController() {
|
fun handleController() {
|
||||||
if (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) !isInPictureInPictureMode else true) {
|
if (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) !isInPictureInPictureMode else true) {
|
||||||
if (playerView.isControllerFullyVisible) {
|
if (playerView.isControllerFullyVisible) {
|
||||||
ObjectAnimator.ofFloat(playerView.findViewById(R.id.exo_controller), "alpha", 1f, 0f)
|
ObjectAnimator.ofFloat(
|
||||||
|
playerView.findViewById(R.id.exo_controller),
|
||||||
|
"alpha",
|
||||||
|
1f,
|
||||||
|
0f
|
||||||
|
)
|
||||||
.setDuration(controllerDuration).start()
|
.setDuration(controllerDuration).start()
|
||||||
ObjectAnimator.ofFloat(playerView.findViewById(R.id.exo_bottom_cont), "translationY", 0f, 128f)
|
ObjectAnimator.ofFloat(
|
||||||
|
playerView.findViewById(R.id.exo_bottom_cont),
|
||||||
|
"translationY",
|
||||||
|
0f,
|
||||||
|
128f
|
||||||
|
)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
ObjectAnimator.ofFloat(playerView.findViewById(R.id.exo_timeline_cont), "translationY", 0f, 128f)
|
ObjectAnimator.ofFloat(
|
||||||
|
playerView.findViewById(R.id.exo_timeline_cont),
|
||||||
|
"translationY",
|
||||||
|
0f,
|
||||||
|
128f
|
||||||
|
)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
ObjectAnimator.ofFloat(playerView.findViewById(R.id.exo_top_cont), "translationY", 0f, -128f)
|
ObjectAnimator.ofFloat(
|
||||||
|
playerView.findViewById(R.id.exo_top_cont),
|
||||||
|
"translationY",
|
||||||
|
0f,
|
||||||
|
-128f
|
||||||
|
)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
playerView.postDelayed({ playerView.hideController() }, controllerDuration)
|
playerView.postDelayed({ playerView.hideController() }, controllerDuration)
|
||||||
} else {
|
} else {
|
||||||
checkNotch()
|
checkNotch()
|
||||||
playerView.showController()
|
playerView.showController()
|
||||||
ObjectAnimator.ofFloat(playerView.findViewById(R.id.exo_controller), "alpha", 0f, 1f)
|
ObjectAnimator.ofFloat(
|
||||||
|
playerView.findViewById(R.id.exo_controller),
|
||||||
|
"alpha",
|
||||||
|
0f,
|
||||||
|
1f
|
||||||
|
)
|
||||||
.setDuration(controllerDuration).start()
|
.setDuration(controllerDuration).start()
|
||||||
ObjectAnimator.ofFloat(playerView.findViewById(R.id.exo_bottom_cont), "translationY", 128f, 0f)
|
ObjectAnimator.ofFloat(
|
||||||
|
playerView.findViewById(R.id.exo_bottom_cont),
|
||||||
|
"translationY",
|
||||||
|
128f,
|
||||||
|
0f
|
||||||
|
)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
ObjectAnimator.ofFloat(playerView.findViewById(R.id.exo_timeline_cont), "translationY", 128f, 0f)
|
ObjectAnimator.ofFloat(
|
||||||
|
playerView.findViewById(R.id.exo_timeline_cont),
|
||||||
|
"translationY",
|
||||||
|
128f,
|
||||||
|
0f
|
||||||
|
)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
ObjectAnimator.ofFloat(playerView.findViewById(R.id.exo_top_cont), "translationY", -128f, 0f)
|
ObjectAnimator.ofFloat(
|
||||||
|
playerView.findViewById(R.id.exo_top_cont),
|
||||||
|
"translationY",
|
||||||
|
-128f,
|
||||||
|
0f
|
||||||
|
)
|
||||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,8 +729,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!settings.doubleTap) {
|
if (!settings.doubleTap) {
|
||||||
playerView.findViewById<View>(R.id.exo_fast_forward_button_cont).visibility = View.VISIBLE
|
playerView.findViewById<View>(R.id.exo_fast_forward_button_cont).visibility =
|
||||||
playerView.findViewById<View>(R.id.exo_fast_rewind_button_cont).visibility = View.VISIBLE
|
View.VISIBLE
|
||||||
|
playerView.findViewById<View>(R.id.exo_fast_rewind_button_cont).visibility =
|
||||||
|
View.VISIBLE
|
||||||
playerView.findViewById<ImageButton>(R.id.exo_fast_forward_button).setOnClickListener {
|
playerView.findViewById<ImageButton>(R.id.exo_fast_forward_button).setOnClickListener {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
seek(true)
|
seek(true)
|
||||||
@@ -696,7 +776,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
|
|
||||||
exoBrightness.addOnChangeListener { _, value, _ ->
|
exoBrightness.addOnChangeListener { _, value, _ ->
|
||||||
val lp = window.attributes
|
val lp = window.attributes
|
||||||
lp.screenBrightness = brightnessConverter((value.takeIf { !it.isNaN() } ?: 0f) / 10, false)
|
lp.screenBrightness =
|
||||||
|
brightnessConverter((value.takeIf { !it.isNaN() } ?: 0f) / 10, false)
|
||||||
window.attributes = lp
|
window.attributes = lp
|
||||||
brightnessHide()
|
brightnessHide()
|
||||||
}
|
}
|
||||||
@@ -757,7 +838,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSingleClick(event: MotionEvent) = if (isSeeking) doubleTap(false, event) else handleController()
|
override fun onSingleClick(event: MotionEvent) =
|
||||||
|
if (isSeeking) doubleTap(false, event) else handleController()
|
||||||
})
|
})
|
||||||
val rewindArea = playerView.findViewById<View>(R.id.exo_rewind_area)
|
val rewindArea = playerView.findViewById<View>(R.id.exo_rewind_area)
|
||||||
rewindArea.isClickable = true
|
rewindArea.isClickable = true
|
||||||
@@ -786,7 +868,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSingleClick(event: MotionEvent) = if (isSeeking) doubleTap(true, event) else handleController()
|
override fun onSingleClick(event: MotionEvent) =
|
||||||
|
if (isSeeking) doubleTap(true, event) else handleController()
|
||||||
})
|
})
|
||||||
val forwardArea = playerView.findViewById<View>(R.id.exo_forward_area)
|
val forwardArea = playerView.findViewById<View>(R.id.exo_forward_area)
|
||||||
forwardArea.isClickable = true
|
forwardArea.isClickable = true
|
||||||
@@ -817,7 +900,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
|
||||||
serverInfo.text = model.watchSources!!.names.getOrNull(media.selected!!.sourceIndex) ?: model.watchSources!!.names[0]
|
serverInfo.text = model.watchSources!!.names.getOrNull(media.selected!!.sourceIndex)
|
||||||
|
?: model.watchSources!!.names[0]
|
||||||
|
|
||||||
model.epChanged.observe(this) {
|
model.epChanged.observe(this) {
|
||||||
epChanging = !it
|
epChanging = !it
|
||||||
@@ -906,7 +990,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
rpc?.send {
|
rpc?.send {
|
||||||
type = RPC.Type.WATCHING
|
type = RPC.Type.WATCHING
|
||||||
activityName = media.userPreferredName
|
activityName = media.userPreferredName
|
||||||
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.episode_num, ep.number)
|
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
|
||||||
|
R.string.episode_num,
|
||||||
|
ep.number
|
||||||
|
)
|
||||||
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}"
|
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}"
|
||||||
media.cover?.let { cover ->
|
media.cover?.let { cover ->
|
||||||
largeImage = RPC.Link(media.userPreferredName, cover)
|
largeImage = RPC.Link(media.userPreferredName, cover)
|
||||||
@@ -922,25 +1009,25 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
//FullScreen
|
//FullScreen
|
||||||
isFullscreen = loadData("${media.id}_fullscreenInt", this) ?: isFullscreen
|
isFullscreen = loadData("${media.id}_fullscreenInt", this) ?: isFullscreen
|
||||||
playerView.resizeMode = when (isFullscreen) {
|
playerView.resizeMode = when (isFullscreen) {
|
||||||
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||||
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||||
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
}
|
}
|
||||||
|
|
||||||
exoScreen.setOnClickListener {
|
exoScreen.setOnClickListener {
|
||||||
if (isFullscreen < 2) isFullscreen += 1 else isFullscreen = 0
|
if (isFullscreen < 2) isFullscreen += 1 else isFullscreen = 0
|
||||||
playerView.resizeMode = when (isFullscreen) {
|
playerView.resizeMode = when (isFullscreen) {
|
||||||
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||||
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||||
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
}
|
}
|
||||||
snackString(
|
snackString(
|
||||||
when (isFullscreen) {
|
when (isFullscreen) {
|
||||||
0 -> "Original"
|
0 -> "Original"
|
||||||
1 -> "Zoom"
|
1 -> "Zoom"
|
||||||
2 -> "Stretch"
|
2 -> "Stretch"
|
||||||
else -> "Original"
|
else -> "Original"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -959,7 +1046,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
|
|
||||||
//Settings
|
//Settings
|
||||||
exoSettings.setOnClickListener {
|
exoSettings.setOnClickListener {
|
||||||
saveData("${media.id}_${media.anime!!.selectedEpisode}", exoPlayer.currentPosition, this)
|
saveData(
|
||||||
|
"${media.id}_${media.anime!!.selectedEpisode}",
|
||||||
|
exoPlayer.currentPosition,
|
||||||
|
this
|
||||||
|
)
|
||||||
val intent = Intent(this, PlayerSettingsActivity::class.java).apply {
|
val intent = Intent(this, PlayerSettingsActivity::class.java).apply {
|
||||||
putExtra("subtitle", subtitle)
|
putExtra("subtitle", subtitle)
|
||||||
}
|
}
|
||||||
@@ -979,7 +1070,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
|
|
||||||
playbackParameters = PlaybackParameters(speeds[curSpeed])
|
playbackParameters = PlaybackParameters(speeds[curSpeed])
|
||||||
var speed: Float
|
var speed: Float
|
||||||
val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.speed))
|
val speedDialog =
|
||||||
|
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.speed))
|
||||||
exoSpeed.setOnClickListener {
|
exoSpeed.setOnClickListener {
|
||||||
speedDialog.setSingleChoiceItems(speedsName, curSpeed) { dialog, i ->
|
speedDialog.setSingleChoiceItems(speedsName, curSpeed) { dialog, i ->
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
@@ -1018,16 +1110,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
|
|
||||||
isFullscreen = settings.resize
|
isFullscreen = settings.resize
|
||||||
playerView.resizeMode = when (settings.resize) {
|
playerView.resizeMode = when (settings.resize) {
|
||||||
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
0 -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
1 -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||||
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
2 -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||||
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
}
|
}
|
||||||
|
|
||||||
preloading = false
|
preloading = false
|
||||||
val showProgressDialog = if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") ?: true else false
|
val showProgressDialog =
|
||||||
|
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
|
||||||
|
?: true else false
|
||||||
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
|
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
|
||||||
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.auto_update, media.userPreferredName))
|
AlertDialog.Builder(this, R.style.DialogTheme)
|
||||||
|
.setTitle(getString(R.string.auto_update, media.userPreferredName))
|
||||||
.apply {
|
.apply {
|
||||||
setOnCancelListener { hideSystemBars() }
|
setOnCancelListener { hideSystemBars() }
|
||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
@@ -1074,16 +1169,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
|
|
||||||
subtitle = intent.getSerialized("subtitle")
|
subtitle = intent.getSerialized("subtitle")
|
||||||
?: when (val subLang: String? = loadData("subLang_${media.id}", this)) {
|
?: when (val subLang: String? = loadData("subLang_${media.id}", this)) {
|
||||||
null -> {
|
null -> {
|
||||||
when (episode.selectedSubtitle) {
|
when (episode.selectedSubtitle) {
|
||||||
null -> null
|
null -> null
|
||||||
-1 -> ext.subtitles.find { it.language.trim() == "English" || it.language == "en-US" }
|
-1 -> ext.subtitles.find { it.language.trim() == "English" || it.language == "en-US" }
|
||||||
else -> ext.subtitles.getOrNull(episode.selectedSubtitle!!)
|
else -> ext.subtitles.getOrNull(episode.selectedSubtitle!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"None" -> ext.subtitles.let { null }
|
"None" -> ext.subtitles.let { null }
|
||||||
else -> ext.subtitles.find { it.language == subLang }
|
else -> ext.subtitles.find { it.language == subLang }
|
||||||
}
|
}
|
||||||
|
|
||||||
//Subtitles
|
//Subtitles
|
||||||
@@ -1091,21 +1186,44 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
exoSubtitle.setOnClickListener {
|
exoSubtitle.setOnClickListener {
|
||||||
subClick()
|
subClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
var sub: MediaItem.SubtitleConfiguration? = null
|
var sub: MediaItem.SubtitleConfiguration? = null
|
||||||
if (subtitle != null) {
|
if (subtitle != null) {
|
||||||
sub = MediaItem.SubtitleConfiguration
|
//var localFile: String? = null
|
||||||
.Builder(Uri.parse(subtitle!!.file.url))
|
if (subtitle?.type == SubtitleType.UNKNOWN) {
|
||||||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
val context = this
|
||||||
.setMimeType(
|
runBlocking {
|
||||||
when (subtitle?.type) {
|
val type = SubtitleDownloader.downloadSubtitles(context, subtitle!!.file.url)
|
||||||
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
val fileUri = Uri.parse(subtitle!!.file.url)
|
||||||
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
sub = MediaItem.SubtitleConfiguration
|
||||||
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
.Builder(fileUri)
|
||||||
else -> MimeTypes.TEXT_UNKNOWN
|
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
||||||
}
|
.setMimeType(
|
||||||
)
|
when (type) {
|
||||||
.build()
|
SubtitleType.VTT -> MimeTypes.TEXT_SSA
|
||||||
|
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
||||||
|
SubtitleType.SRT -> MimeTypes.TEXT_SSA
|
||||||
|
else -> MimeTypes.TEXT_SSA
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setId("69")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
println("sub: $sub")
|
||||||
|
} else {
|
||||||
|
sub = MediaItem.SubtitleConfiguration
|
||||||
|
.Builder(Uri.parse(subtitle!!.file.url))
|
||||||
|
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
||||||
|
.setMimeType(
|
||||||
|
when (subtitle?.type) {
|
||||||
|
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
||||||
|
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
||||||
|
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
||||||
|
else -> MimeTypes.TEXT_UNKNOWN
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setId("69")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@@ -1113,7 +1231,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val but = playerView.findViewById<ImageButton>(R.id.exo_download)
|
val but = playerView.findViewById<ImageButton>(R.id.exo_download)
|
||||||
if (video?.format == VideoType.CONTAINER || (loadData<Int>("settings_download_manager") ?: 0) != 0) {
|
if (video?.format == VideoType.CONTAINER || (loadData<Int>("settings_download_manager")
|
||||||
|
?: 0) != 0
|
||||||
|
) {
|
||||||
but.visibility = View.VISIBLE
|
but.visibility = View.VISIBLE
|
||||||
but.setOnClickListener {
|
but.setOnClickListener {
|
||||||
download(this, episode, animeTitle.text.toString())
|
download(this, episode, animeTitle.text.toString())
|
||||||
@@ -1146,12 +1266,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
val mimeType = when (video?.format) {
|
val mimeType = when (video?.format) {
|
||||||
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||||
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
||||||
else -> MimeTypes.APPLICATION_MP4
|
else -> MimeTypes.APPLICATION_MP4
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
|
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
|
||||||
|
|
||||||
if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
|
if (sub != null) {
|
||||||
|
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
|
||||||
|
builder.setSubtitleConfigurations(listofnotnullsubs)
|
||||||
|
}
|
||||||
mediaItem = builder.build()
|
mediaItem = builder.build()
|
||||||
|
|
||||||
//Source
|
//Source
|
||||||
@@ -1163,8 +1286,23 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
trackSelector = DefaultTrackSelector(this)
|
trackSelector = DefaultTrackSelector(this)
|
||||||
trackSelector.setParameters(
|
trackSelector.setParameters(
|
||||||
trackSelector.buildUponParameters()
|
trackSelector.buildUponParameters()
|
||||||
.setMinVideoSize(loadData("maxWidth", this) ?: 720, loadData("maxHeight", this) ?: 480)
|
.setAllowVideoMixedMimeTypeAdaptiveness(true)
|
||||||
|
.setAllowVideoNonSeamlessAdaptiveness(true)
|
||||||
|
.setSelectUndeterminedTextLanguage(true)
|
||||||
|
.setAllowAudioMixedMimeTypeAdaptiveness(true)
|
||||||
|
.setAllowMultipleAdaptiveSelections(true)
|
||||||
|
.setPreferredTextLanguage(subtitle?.language ?: "en")
|
||||||
|
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||||
|
.setRendererDisabled(C.TRACK_TYPE_VIDEO, false)
|
||||||
|
.setRendererDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||||
|
.setRendererDisabled(C.TRACK_TYPE_TEXT, false)
|
||||||
|
.setMinVideoSize(
|
||||||
|
loadData("maxWidth", this) ?: 720,
|
||||||
|
loadData("maxHeight", this) ?: 480
|
||||||
|
)
|
||||||
.setMaxVideoSize(1, 1)
|
.setMaxVideoSize(1, 1)
|
||||||
|
//.setOverrideForType(
|
||||||
|
// TrackSelectionOverride(trackSelector, 2))
|
||||||
)
|
)
|
||||||
|
|
||||||
if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) {
|
if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) {
|
||||||
@@ -1181,7 +1319,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.continue_from, time)).apply {
|
AlertDialog.Builder(this, R.style.DialogTheme)
|
||||||
|
.setTitle(getString(R.string.continue_from, time)).apply {
|
||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
setPositiveButton(getString(R.string.yes)) { d, _ ->
|
setPositiveButton(getString(R.string.yes)) { d, _ ->
|
||||||
buildExoplayer()
|
buildExoplayer()
|
||||||
@@ -1214,6 +1353,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
playerView.player = exoPlayer
|
playerView.player = exoPlayer
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mediaSession = MediaSession.Builder(this, exoPlayer).build()
|
mediaSession = MediaSession.Builder(this, exoPlayer).build()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1221,8 +1361,34 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exoPlayer.addListener(this)
|
exoPlayer.addListener(this)
|
||||||
|
exoPlayer.addAnalyticsListener(EventLogger())
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
/*private fun selectSubtitleTrack() {
|
||||||
|
// Get the current track groups
|
||||||
|
val trackGroups = exoPlayer.currentTrackGroups
|
||||||
|
|
||||||
|
// Prepare a track selector parameters builder
|
||||||
|
val parametersBuilder = DefaultTrackSelector.ParametersBuilder(this)
|
||||||
|
|
||||||
|
// Iterate through the track groups to find the subtitle tracks
|
||||||
|
for (i in 0 until trackGroups.length) {
|
||||||
|
val trackGroup = trackGroups[i]
|
||||||
|
for (j in 0 until trackGroup.length) {
|
||||||
|
val trackMetadata = trackGroup.getFormat(j)
|
||||||
|
|
||||||
|
// Check if the track is a subtitle track
|
||||||
|
if (MimeTypes.isText(trackMetadata.sampleMimeType)) {
|
||||||
|
parametersBuilder.setRendererDisabled(i, false) // Enable the renderer for this track group
|
||||||
|
parametersBuilder.setSelectionOverride(i, trackGroups, DefaultTrackSelector.SelectionOverride(j, 0)) // Override to select this track
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the track selector parameters to select the subtitle
|
||||||
|
trackSelector.setParameters(parametersBuilder)
|
||||||
|
}*/
|
||||||
|
|
||||||
private fun releasePlayer() {
|
private fun releasePlayer() {
|
||||||
isPlayerPlaying = exoPlayer.playWhenReady
|
isPlayerPlaying = exoPlayer.playWhenReady
|
||||||
@@ -1266,7 +1432,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
orientationListener?.disable()
|
orientationListener?.disable()
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
playerView.player?.pause()
|
playerView.player?.pause()
|
||||||
saveData("${media.id}_${media.anime!!.selectedEpisode}", exoPlayer.currentPosition, this)
|
saveData(
|
||||||
|
"${media.id}_${media.anime!!.selectedEpisode}",
|
||||||
|
exoPlayer.currentPosition,
|
||||||
|
this
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1304,7 +1474,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
playerView.keepScreenOn = isPlaying
|
playerView.keepScreenOn = isPlaying
|
||||||
(exoPlay.drawable as Animatable?)?.start()
|
(exoPlay.drawable as Animatable?)?.start()
|
||||||
if (!this.isDestroyed) Glide.with(this)
|
if (!this.isDestroyed) Glide.with(this)
|
||||||
.load(if (isPlaying) R.drawable.anim_play_to_pause else R.drawable.anim_pause_to_play).into(exoPlay)
|
.load(if (isPlaying) R.drawable.anim_play_to_pause else R.drawable.anim_pause_to_play)
|
||||||
|
.into(exoPlay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1383,7 +1554,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (settings.autoSkipOPED && (new.skipType == "op" || new.skipType == "ed") && !skippedTimeStamps.contains(new)) {
|
if (settings.autoSkipOPED && (new.skipType == "op" || new.skipType == "ed") && !skippedTimeStamps.contains(
|
||||||
|
new
|
||||||
|
)
|
||||||
|
) {
|
||||||
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
||||||
skippedTimeStamps.add(new)
|
skippedTimeStamps.add(new)
|
||||||
}
|
}
|
||||||
@@ -1400,6 +1574,27 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
|
tracks.groups.forEach {
|
||||||
|
println("Track__: $it")
|
||||||
|
println("Track__: ${it.length}")
|
||||||
|
println("Track__: ${it.isSelected}")
|
||||||
|
println("Track__: ${it.type}")
|
||||||
|
println("Track__: ${it.mediaTrackGroup.id}")
|
||||||
|
if (it.type == 3 && it.mediaTrackGroup.id == "1:"){
|
||||||
|
playerView.player?.trackSelectionParameters =
|
||||||
|
playerView.player?.trackSelectionParameters?.buildUpon()
|
||||||
|
?.setOverrideForType(
|
||||||
|
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1))
|
||||||
|
?.build()!!
|
||||||
|
}else if(it.type == 3){
|
||||||
|
playerView.player?.trackSelectionParameters =
|
||||||
|
playerView.player?.trackSelectionParameters?.buildUpon()
|
||||||
|
?.addOverride(
|
||||||
|
TrackSelectionOverride(it.mediaTrackGroup, listOf()))
|
||||||
|
?.build()!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println("Track: ${tracks.groups.size}")
|
||||||
if (tracks.groups.size <= 2) exoQuality.visibility = View.GONE
|
if (tracks.groups.size <= 2) exoQuality.visibility = View.GONE
|
||||||
else {
|
else {
|
||||||
exoQuality.visibility = View.VISIBLE
|
exoQuality.visibility = View.VISIBLE
|
||||||
@@ -1426,6 +1621,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
private var isBuffering = true
|
private var isBuffering = true
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
if (playbackState == ExoPlayer.STATE_READY) {
|
if (playbackState == ExoPlayer.STATE_READY) {
|
||||||
|
|
||||||
exoPlayer.play()
|
exoPlayer.play()
|
||||||
if (episodeLength == 0f) {
|
if (episodeLength == 0f) {
|
||||||
episodeLength = exoPlayer.duration.toFloat()
|
episodeLength = exoPlayer.duration.toFloat()
|
||||||
@@ -1453,7 +1649,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
var i = 1
|
var i = 1
|
||||||
while (isFiller) {
|
while (isFiller) {
|
||||||
if (episodeArr.size > currentEpisodeIndex + i) {
|
if (episodeArr.size > currentEpisodeIndex + i) {
|
||||||
isFiller = if (settings.autoSkipFiller) episodes[episodeArr[currentEpisodeIndex + i]]?.filler ?: false else false
|
isFiller =
|
||||||
|
if (settings.autoSkipFiller) episodes[episodeArr[currentEpisodeIndex + i]]?.filler
|
||||||
|
?: false else false
|
||||||
if (!isFiller) runnable.invoke(i)
|
if (!isFiller) runnable.invoke(i)
|
||||||
i++
|
i++
|
||||||
} else {
|
} else {
|
||||||
@@ -1509,7 +1707,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
shareVideo.setDataAndType(Uri.parse(videoURL), "video/*")
|
shareVideo.setDataAndType(Uri.parse(videoURL), "video/*")
|
||||||
shareVideo.setPackage("com.instantbits.cast.webvideo")
|
shareVideo.setPackage("com.instantbits.cast.webvideo")
|
||||||
if (subtitle != null) shareVideo.putExtra("subtitle", subtitle!!.file.url)
|
if (subtitle != null) shareVideo.putExtra("subtitle", subtitle!!.file.url)
|
||||||
shareVideo.putExtra("title", media.userPreferredName + " : Ep " + episodeTitleArr[currentEpisodeIndex])
|
shareVideo.putExtra(
|
||||||
|
"title",
|
||||||
|
media.userPreferredName + " : Ep " + episodeTitleArr[currentEpisodeIndex]
|
||||||
|
)
|
||||||
shareVideo.putExtra("poster", episode.thumb?.url ?: media.cover)
|
shareVideo.putExtra("poster", episode.thumb?.url ?: media.cover)
|
||||||
val headers = Bundle()
|
val headers = Bundle()
|
||||||
defaultHeaders.forEach {
|
defaultHeaders.forEach {
|
||||||
@@ -1579,7 +1780,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
override fun onPictureInPictureModeChanged(
|
||||||
|
isInPictureInPictureMode: Boolean,
|
||||||
|
newConfig: Configuration
|
||||||
|
) {
|
||||||
onPiPChanged(isInPictureInPictureMode)
|
onPiPChanged(isInPictureInPictureMode)
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package ani.dantotsu.media.anime
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -23,6 +25,7 @@ import ani.dantotsu.media.MediaDetailsViewModel
|
|||||||
import ani.dantotsu.others.Download.download
|
import ani.dantotsu.others.Download.download
|
||||||
import ani.dantotsu.parsers.VideoExtractor
|
import ani.dantotsu.parsers.VideoExtractor
|
||||||
import ani.dantotsu.parsers.VideoType
|
import ani.dantotsu.parsers.VideoType
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -52,6 +55,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
_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
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class MangaChapterAdapter(
|
|||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||||
val ep = arr[position]
|
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 (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.itemEpisodeViewedCover.visibility = View.VISIBLE
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
@@ -16,12 +17,17 @@ import ani.dantotsu.databinding.ItemChipBinding
|
|||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
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.MangaReadSources
|
||||||
|
import ani.dantotsu.parsers.MangaSources
|
||||||
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.lang.IndexOutOfBoundsException
|
||||||
|
|
||||||
class MangaReadAdapter(
|
class MangaReadAdapter(
|
||||||
private val media: Media,
|
private val media: Media,
|
||||||
@@ -49,10 +55,10 @@ class MangaReadAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Source Selection
|
//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) {
|
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||||
binding.animeSource.setText(mangaReadSources.names[source])
|
binding.animeSource.setText(mangaReadSources.names[source])
|
||||||
|
|
||||||
mangaReadSources[source].apply {
|
mangaReadSources[source].apply {
|
||||||
binding.animeSourceTitle.text = showUserText
|
binding.animeSourceTitle.text = showUserText
|
||||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
@@ -64,9 +70,34 @@ class MangaReadAdapter(
|
|||||||
fragment.onSourceChange(i).apply {
|
fragment.onSourceChange(i).apply {
|
||||||
binding.animeSourceTitle.text = showUserText
|
binding.animeSourceTitle.text = showUserText
|
||||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||||
|
source = i
|
||||||
|
setLanguageList(0,i)
|
||||||
}
|
}
|
||||||
subscribeButton(false)
|
subscribeButton(false)
|
||||||
fragment.loadChapters(i)
|
fragment.loadChapters(i, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
//Subscription
|
||||||
@@ -145,6 +176,7 @@ class MangaReadAdapter(
|
|||||||
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
|
||||||
}
|
}
|
||||||
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||||
|
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color))
|
||||||
|
|
||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
selected()
|
selected()
|
||||||
@@ -174,7 +206,9 @@ class MangaReadAdapter(
|
|||||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||||
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
|
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
|
||||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||||
if (chapters.contains(continueEp)) {
|
val formattedChapters = chapters.map { MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() }
|
||||||
|
if (formattedChapters.contains(continueEp)) {
|
||||||
|
continueEp = chapters[formattedChapters.indexOf(continueEp)]
|
||||||
binding.animeSourceContinue.visibility = View.VISIBLE
|
binding.animeSourceContinue.visibility = View.VISIBLE
|
||||||
handleProgress(
|
handleProgress(
|
||||||
binding.itemEpisodeProgressCont,
|
binding.itemEpisodeProgressCont,
|
||||||
@@ -220,6 +254,25 @@ 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
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package ani.dantotsu.media.manga
|
package ani.dantotsu.media.manga
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.math.MathUtils.clamp
|
import androidx.core.math.MathUtils.clamp
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@@ -13,20 +17,30 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.parsers.HMangaSources
|
import ani.dantotsu.parsers.HMangaSources
|
||||||
import ani.dantotsu.parsers.MangaParser
|
import ani.dantotsu.parsers.MangaParser
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.parsers.MangaSources
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
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
|
||||||
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
|
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
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.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
@@ -185,8 +199,16 @@ open class MangaReadFragment : Fragment() {
|
|||||||
return model.mangaReadSources?.get(i)!!
|
return model.mangaReadSources?.get(i)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadChapters(i: Int) {
|
fun onLangChange(i: Int) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) }
|
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) {
|
fun onIconPressed(viewType: Int, rev: Boolean) {
|
||||||
@@ -225,6 +247,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) {
|
fun onMangaChapterClick(i: String) {
|
||||||
model.continueMedia = false
|
model.continueMedia = false
|
||||||
media.manga?.chapters?.get(i)?.let {
|
media.manga?.chapters?.get(i)?.let {
|
||||||
|
|||||||
@@ -116,10 +116,10 @@ abstract class BaseImageAdapter(
|
|||||||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/*suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
suspend fun Context.loadBitmap_old(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
||||||
return tryWithSuspend {
|
return tryWithSuspend {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Glide.with(this@loadBitmap)
|
Glide.with(this@loadBitmap_old)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.let {
|
.let {
|
||||||
if (link.url.startsWith("file://")) {
|
if (link.url.startsWith("file://")) {
|
||||||
@@ -142,7 +142,7 @@ abstract class BaseImageAdapter(
|
|||||||
.get()
|
.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
|
||||||
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
|
||||||
return tryWithSuspend {
|
return tryWithSuspend {
|
||||||
@@ -156,10 +156,6 @@ abstract class BaseImageAdapter(
|
|||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
} else {
|
} else {
|
||||||
println("bitmap from cache")
|
|
||||||
println(link.url)
|
|
||||||
println(mangaCache.get(link.url))
|
|
||||||
println("cache size: ${mangaCache.size()}")
|
|
||||||
mangaCache.get(link.url)?.let { imageData ->
|
mangaCache.get(link.url)?.let { imageData ->
|
||||||
val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap)
|
val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap)
|
||||||
it.load(bitmap)
|
it.load(bitmap)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package ani.dantotsu.media.manga.mangareader
|
package ani.dantotsu.media.manga.mangareader
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -63,6 +65,12 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
|
_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
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.*
|
|||||||
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
|
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
|
||||||
import ani.dantotsu.settings.ReaderSettings
|
import ani.dantotsu.settings.ReaderSettings
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
@@ -125,6 +126,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
|
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import ani.dantotsu.loadImage
|
|||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.parsers.ShowResponse
|
import ani.dantotsu.parsers.ShowResponse
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import ani.dantotsu.settings.CurrentReaderSettings
|
|||||||
import ani.dantotsu.settings.NovelReaderSettings
|
import ani.dantotsu.settings.NovelReaderSettings
|
||||||
import ani.dantotsu.settings.UserInterfaceSettings
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.tryWith
|
import ani.dantotsu.tryWith
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import com.vipulog.ebookreader.Book
|
import com.vipulog.ebookreader.Book
|
||||||
@@ -135,6 +136,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
|
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package ani.dantotsu.media.user
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
@@ -12,6 +15,9 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityListBinding
|
import ani.dantotsu.databinding.ActivityListBinding
|
||||||
|
import ani.dantotsu.loadData
|
||||||
|
import ani.dantotsu.settings.UserInterfaceSettings
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -26,10 +32,39 @@ class ListActivity : AppCompatActivity() {
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityListBinding.inflate(layoutInflater)
|
binding = ActivityListBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, typedValue, true)
|
||||||
|
val primaryColor = typedValue.data
|
||||||
|
val typedValue2 = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true)
|
||||||
|
val primaryTextColor = typedValue2.data
|
||||||
|
val typedValue3 = TypedValue()
|
||||||
|
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue3, true)
|
||||||
|
val secondaryColor = typedValue3.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.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||||
|
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||||
|
if (!uiSettings.immersiveMode) {
|
||||||
|
this.window.statusBarColor =
|
||||||
|
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||||
|
binding.root.fitsSystemWindows = true
|
||||||
|
|
||||||
|
}else{
|
||||||
|
binding.root.fitsSystemWindows = false
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||||
|
}
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
|
||||||
val anime = intent.getBooleanExtra("anime", true)
|
val anime = intent.getBooleanExtra("anime", true)
|
||||||
binding.listTitle.text = intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
|
binding.listTitle.text = intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import ani.dantotsu.databinding.FragmentListBinding
|
|||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaAdaptor
|
import ani.dantotsu.media.MediaAdaptor
|
||||||
import ani.dantotsu.media.OtherDetailsViewModel
|
import ani.dantotsu.media.OtherDetailsViewModel
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class ListFragment : Fragment() {
|
class ListFragment : Fragment() {
|
||||||
private var _binding: FragmentListBinding? = null
|
private var _binding: FragmentListBinding? = null
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package ani.dantotsu.others
|
package ani.dantotsu.others
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -46,6 +48,12 @@ open class CustomBottomDialog : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = BottomSheetCustomBinding.inflate(inflater, container, false)
|
_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
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import ani.dantotsu.FileUrl
|
|||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.BottomSheetImageBinding
|
import ani.dantotsu.databinding.BottomSheetImageBinding
|
||||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
|
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
|
||||||
|
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap_old
|
||||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
import ani.dantotsu.saveImageToDownloads
|
import ani.dantotsu.saveImageToDownloads
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.shareImage
|
import ani.dantotsu.shareImage
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
@@ -72,12 +74,17 @@ class ImageViewDialog : BottomSheetDialogFragment() {
|
|||||||
if (image2 != null) openLinkInBrowser(image2.url)
|
if (image2 != null) openLinkInBrowser(image2.url)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
val context = requireContext()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val binding = _binding ?: return@launch
|
val binding = _binding ?: return@launch
|
||||||
|
|
||||||
var bitmap = requireContext().loadBitmap(image, trans1 ?: listOf())
|
var bitmap = context.loadBitmap_old(image, trans1 ?: listOf())
|
||||||
val bitmap2 = if (image2 != null) requireContext().loadBitmap(image2, trans2 ?: listOf()) else null
|
var bitmap2 = if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null
|
||||||
|
if (bitmap == 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
|
bitmap = if (bitmap2 != null && bitmap != null) mergeBitmap(bitmap, bitmap2,) else bitmap
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ani.dantotsu.R
|
|||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.databinding.ActivityImageSearchBinding
|
import ani.dantotsu.databinding.ActivityImageSearchBinding
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -46,6 +47,7 @@ class ImageSearchActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityImageSearchBinding.inflate(layoutInflater)
|
binding = ActivityImageSearchBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ abstract class AnimeParser : BaseParser() {
|
|||||||
* Returns null, if no latest episode is found.
|
* Returns null, if no latest episode is found.
|
||||||
* **/
|
* **/
|
||||||
open suspend fun getLatestEpisode(animeLink: String, extra: Map<String, String>?, sAnime: SAnime, latest: Float): Episode?{
|
open suspend fun getLatestEpisode(animeLink: String, extra: Map<String, String>?, sAnime: SAnime, latest: Float): Episode?{
|
||||||
return loadEpisodes(animeLink, extra, sAnime)
|
val episodes = loadEpisodes(animeLink, extra, sAnime)
|
||||||
|
val max = episodes
|
||||||
.maxByOrNull { it.number.toFloatOrNull()?:0f }
|
.maxByOrNull { it.number.toFloatOrNull()?:0f }
|
||||||
|
return max
|
||||||
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
|
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import ani.dantotsu.currContext
|
|||||||
import ani.dantotsu.logger
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.media.manga.ImageData
|
import ani.dantotsu.media.manga.ImageData
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
|
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.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
@@ -38,7 +40,10 @@ import kotlinx.coroutines.async
|
|||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@@ -59,6 +64,7 @@ class AniyomiAdapter {
|
|||||||
|
|
||||||
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
val extension: AnimeExtension.Installed
|
val extension: AnimeExtension.Installed
|
||||||
|
var sourceLanguage = 0
|
||||||
init {
|
init {
|
||||||
this.extension = extension
|
this.extension = extension
|
||||||
}
|
}
|
||||||
@@ -68,7 +74,12 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||||||
override val isDubAvailableSeparately = false
|
override val isDubAvailableSeparately = false
|
||||||
override val isNSFW = extension.isNsfw
|
override val isNSFW = extension.isNsfw
|
||||||
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
|
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
|
||||||
val source = extension.sources.first()
|
val source = try{
|
||||||
|
extension.sources[sourceLanguage]
|
||||||
|
}catch (e: Exception){
|
||||||
|
sourceLanguage = 0
|
||||||
|
extension.sources[sourceLanguage]
|
||||||
|
}
|
||||||
if (source is AnimeCatalogueSource) {
|
if (source is AnimeCatalogueSource) {
|
||||||
try {
|
try {
|
||||||
val res = source.getEpisodeList(sAnime)
|
val res = source.getEpisodeList(sAnime)
|
||||||
@@ -88,7 +99,12 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> {
|
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> {
|
||||||
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 {
|
return try {
|
||||||
val videos = source.getVideoList(sEpisode)
|
val videos = source.getVideoList(sEpisode)
|
||||||
@@ -105,8 +121,12 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<ShowResponse> {
|
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 {
|
return try {
|
||||||
val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first()
|
val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first()
|
||||||
convertAnimesPageToShowResponse(res)
|
convertAnimesPageToShowResponse(res)
|
||||||
@@ -171,6 +191,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||||||
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
val mangaCache = Injekt.get<MangaCache>()
|
val mangaCache = Injekt.get<MangaCache>()
|
||||||
val extension: MangaExtension.Installed
|
val extension: MangaExtension.Installed
|
||||||
|
var sourceLanguage = 0
|
||||||
init {
|
init {
|
||||||
this.extension = extension
|
this.extension = extension
|
||||||
}
|
}
|
||||||
@@ -180,7 +201,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||||||
override val isNSFW = extension.isNsfw
|
override val isNSFW = extension.isNsfw
|
||||||
|
|
||||||
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
|
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
|
||||||
val source = extension.sources.first() as? CatalogueSource ?: return emptyList()
|
val source = try{
|
||||||
|
extension.sources[sourceLanguage]
|
||||||
|
}catch (e: Exception){
|
||||||
|
sourceLanguage = 0
|
||||||
|
extension.sources[sourceLanguage]
|
||||||
|
} as? HttpSource ?: return emptyList()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val res = source.getChapterList(sManga)
|
val res = source.getChapterList(sManga)
|
||||||
@@ -198,7 +224,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||||||
|
|
||||||
|
|
||||||
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||||
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 coroutineScope {
|
return coroutineScope {
|
||||||
try {
|
try {
|
||||||
@@ -318,7 +349,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||||||
|
|
||||||
|
|
||||||
override suspend fun search(query: String): List<ShowResponse> {
|
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 {
|
return try {
|
||||||
val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
|
val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
|
||||||
@@ -498,7 +534,24 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TrackToSubtitle(track: Track, type: SubtitleType = SubtitleType.VTT): Subtitle {
|
private fun TrackToSubtitle(track: Track): Subtitle {
|
||||||
return Subtitle(track.lang, track.url, type)
|
//use Dispatchers.IO to make a HTTP request to determine the subtitle type
|
||||||
|
var type: SubtitleType? = null
|
||||||
|
runBlocking {
|
||||||
|
type = findSubtitleType(track.url)
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
url.endsWith(".vtt", true) -> SubtitleType.VTT
|
||||||
|
url.endsWith(".ass", true) -> SubtitleType.ASS
|
||||||
|
url.endsWith(".srt", true) -> SubtitleType.SRT
|
||||||
|
else -> SubtitleType.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
return type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,17 +57,17 @@ abstract class BaseParser {
|
|||||||
setUserText("Searching : ${mediaObj.mainName()}")
|
setUserText("Searching : ${mediaObj.mainName()}")
|
||||||
val results = search(mediaObj.mainName())
|
val results = search(mediaObj.mainName())
|
||||||
val sortedResults = if (results.isNotEmpty()) {
|
val sortedResults = if (results.isNotEmpty()) {
|
||||||
results.sortedByDescending { FuzzySearch.ratio(it.name, mediaObj.mainName()) }
|
results.sortedByDescending { FuzzySearch.ratio(it.name.lowercase(), mediaObj.mainName().lowercase()) }
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
response = sortedResults.firstOrNull()
|
response = sortedResults.firstOrNull()
|
||||||
|
|
||||||
if (response == null || FuzzySearch.ratio(response.name, mediaObj.mainName()) < 100) {
|
if (response == null || FuzzySearch.ratio(response.name.lowercase(), mediaObj.mainName().lowercase()) < 100) {
|
||||||
setUserText("Searching : ${mediaObj.nameRomaji}")
|
setUserText("Searching : ${mediaObj.nameRomaji}")
|
||||||
val romajiResults = search(mediaObj.nameRomaji)
|
val romajiResults = search(mediaObj.nameRomaji)
|
||||||
val sortedRomajiResults = if (romajiResults.isNotEmpty()) {
|
val sortedRomajiResults = if (romajiResults.isNotEmpty()) {
|
||||||
romajiResults.sortedByDescending { FuzzySearch.ratio(it.name, mediaObj.nameRomaji) }
|
romajiResults.sortedByDescending { FuzzySearch.ratio(it.name.lowercase(), mediaObj.nameRomaji.lowercase()) }
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
@@ -78,10 +78,10 @@ abstract class BaseParser {
|
|||||||
logger("No exact match found in results. Using closest match from RomajiResults.")
|
logger("No exact match found in results. Using closest match from RomajiResults.")
|
||||||
closestRomaji
|
closestRomaji
|
||||||
} else {
|
} else {
|
||||||
val romajiRatio = FuzzySearch.ratio(closestRomaji?.name ?: "", mediaObj.nameRomaji)
|
val romajiRatio = FuzzySearch.ratio(closestRomaji?.name?.lowercase() ?: "", mediaObj.nameRomaji.lowercase())
|
||||||
val mainNameRatio = FuzzySearch.ratio(response.name, mediaObj.mainName())
|
val mainNameRatio = FuzzySearch.ratio(response.name.lowercase(), mediaObj.mainName().lowercase())
|
||||||
logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name}")
|
logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name.lowercase()}")
|
||||||
logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name ?: "None"}")
|
logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name?.lowercase() ?: "None"}")
|
||||||
|
|
||||||
if (romajiRatio > mainNameRatio) {
|
if (romajiRatio > mainNameRatio) {
|
||||||
logger("RomajiResults has a closer match. Replacing response.")
|
logger("RomajiResults has a closer match. Replacing response.")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ani.dantotsu.parsers
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import ani.dantotsu.FileUrl
|
import ani.dantotsu.FileUrl
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
@@ -23,9 +24,11 @@ abstract class MangaParser : BaseParser() {
|
|||||||
* Returns null, if no latest chapter is found.
|
* Returns null, if no latest chapter is found.
|
||||||
* **/
|
* **/
|
||||||
open suspend fun getLatestChapter(mangaLink: String, extra: Map<String, String>?, sManga: SManga, latest: Float): MangaChapter? {
|
open suspend fun getLatestChapter(mangaLink: String, extra: Map<String, String>?, sManga: SManga, latest: Float): MangaChapter? {
|
||||||
return loadChapters(mangaLink, extra, sManga)
|
val chapter = loadChapters(mangaLink, extra, sManga)
|
||||||
.maxByOrNull { it.number.toFloatOrNull() ?: 0f }
|
val max = chapter
|
||||||
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
|
.maxByOrNull { MangaNameAdapter.findChapterNumber(it.number) ?: 0f }
|
||||||
|
return max
|
||||||
|
?.takeIf { latest < (MangaNameAdapter.findChapterNumber(it.number) ?: 0.001f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,22 +36,6 @@ abstract class MangaParser : BaseParser() {
|
|||||||
* **/
|
* **/
|
||||||
abstract suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage>
|
abstract suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage>
|
||||||
|
|
||||||
/*override suspend fun autoSearch(mediaObj: Media): ShowResponse? {
|
|
||||||
var response = loadSavedShowResponse(mediaObj.id)
|
|
||||||
if (response != null) {
|
|
||||||
saveShowResponse(mediaObj.id, response, true)
|
|
||||||
} else {
|
|
||||||
setUserText("Searching : ${mediaObj.mangaName()}")
|
|
||||||
response = search(mediaObj.mangaName()).let { if (it.isNotEmpty()) it[0] else null }
|
|
||||||
|
|
||||||
if (response == null) {
|
|
||||||
setUserText("Searching : ${mediaObj.nameRomaji}")
|
|
||||||
response = search(mediaObj.nameRomaji).let { if (it.isNotEmpty()) it[0] else null }
|
|
||||||
}
|
|
||||||
saveShowResponse(mediaObj.id, response)
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}*/
|
|
||||||
|
|
||||||
open fun getTransformation(): BitmapTransformation? = null
|
open fun getTransformation(): BitmapTransformation? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,5 +159,5 @@ enum class VideoType{
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class SubtitleType{
|
enum class SubtitleType{
|
||||||
VTT, ASS, SRT
|
VTT, ASS, SRT, UNKNOWN
|
||||||
}
|
}
|
||||||
@@ -2,144 +2,45 @@ package ani.dantotsu.settings
|
|||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
|
import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding
|
||||||
import ani.dantotsu.loadData
|
import ani.dantotsu.settings.paging.AnimeExtensionAdapter
|
||||||
import com.bumptech.glide.Glide
|
import ani.dantotsu.settings.paging.AnimeExtensionsViewModel
|
||||||
|
import ani.dantotsu.settings.paging.AnimeExtensionsViewModelFactory
|
||||||
|
import ani.dantotsu.settings.paging.OnAnimeInstallClickListener
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class AnimeExtensionsFragment : Fragment(),
|
class AnimeExtensionsFragment : Fragment(),
|
||||||
SearchQueryHandler {
|
SearchQueryHandler, OnAnimeInstallClickListener {
|
||||||
private var _binding: FragmentAnimeExtensionsBinding? = null
|
private var _binding: FragmentAnimeExtensionsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
private val viewModel: AnimeExtensionsViewModel by viewModels {
|
||||||
|
AnimeExtensionsViewModelFactory(animeExtensionManager)
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var extensionsRecyclerView: RecyclerView
|
private val adapter by lazy {
|
||||||
private lateinit var allextenstionsRecyclerView: RecyclerView
|
AnimeExtensionAdapter(this)
|
||||||
private val animeExtensionManager: AnimeExtensionManager = Injekt.get<AnimeExtensionManager>()
|
}
|
||||||
private val extensionsAdapter = AnimeExtensionsAdapter({ pkg ->
|
|
||||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
|
||||||
val context = requireContext() // Store context in a variable
|
|
||||||
val notificationManager =
|
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
|
|
||||||
|
|
||||||
if (pkg.hasUpdate) {
|
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
|
||||||
animeExtensionManager.updateExtension(pkg)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
|
||||||
.subscribe(
|
|
||||||
{ installStep ->
|
|
||||||
val builder = NotificationCompat.Builder(
|
|
||||||
context,
|
|
||||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
|
||||||
)
|
|
||||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
|
||||||
.setContentTitle("Updating extension")
|
|
||||||
.setContentText("Step: $installStep")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
||||||
notificationManager.notify(1, builder.build())
|
|
||||||
},
|
|
||||||
{ error ->
|
|
||||||
Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error
|
|
||||||
val builder = NotificationCompat.Builder(
|
|
||||||
context,
|
|
||||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
|
||||||
)
|
|
||||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
|
||||||
.setContentTitle("Update failed")
|
|
||||||
.setContentText("Error: ${error.message}")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
notificationManager.notify(1, builder.build())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
val builder = NotificationCompat.Builder(
|
|
||||||
context,
|
|
||||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
|
||||||
)
|
|
||||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
|
||||||
.setContentTitle("Update complete")
|
|
||||||
.setContentText("The extension has been successfully updated.")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
||||||
notificationManager.notify(1, builder.build())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
animeExtensionManager.uninstallExtension(pkg.pkgName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, skipIcons)
|
|
||||||
|
|
||||||
private val allExtensionsAdapter = AllAnimeExtensionsAdapter(lifecycleScope, { pkgName ->
|
|
||||||
|
|
||||||
val notificationManager =
|
|
||||||
requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
|
|
||||||
// Start the installation process
|
|
||||||
animeExtensionManager.installExtension(pkgName)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(
|
|
||||||
{ installStep ->
|
|
||||||
val builder = NotificationCompat.Builder(
|
|
||||||
requireContext(),
|
|
||||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
|
||||||
)
|
|
||||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
|
||||||
.setContentTitle("Installing extension")
|
|
||||||
.setContentText("Step: $installStep")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
||||||
notificationManager.notify(1, builder.build())
|
|
||||||
},
|
|
||||||
{ error ->
|
|
||||||
val builder = NotificationCompat.Builder(
|
|
||||||
requireContext(),
|
|
||||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
|
||||||
)
|
|
||||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
|
||||||
.setContentTitle("Installation failed")
|
|
||||||
.setContentText("Error: ${error.message}")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
notificationManager.notify(1, builder.build())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
val builder = NotificationCompat.Builder(
|
|
||||||
requireContext(),
|
|
||||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
|
||||||
)
|
|
||||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
|
||||||
.setContentTitle("Installation complete")
|
|
||||||
.setContentText("The extension has been successfully installed.")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
||||||
notificationManager.notify(1, builder.build())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}, skipIcons)
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -148,45 +49,72 @@ class AnimeExtensionsFragment : Fragment(),
|
|||||||
): View {
|
): View {
|
||||||
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
|
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
extensionsRecyclerView = binding.animeExtensionsRecyclerView
|
binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = false
|
||||||
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
binding.allAnimeExtensionsRecyclerView.adapter = adapter
|
||||||
extensionsRecyclerView.adapter = extensionsAdapter
|
binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
|
(binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true
|
||||||
allextenstionsRecyclerView = binding.allAnimeExtensionsRecyclerView
|
|
||||||
allextenstionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
allextenstionsRecyclerView.adapter = allExtensionsAdapter
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
animeExtensionManager.installedExtensionsFlow.collect { extensions ->
|
viewModel.pagerFlow.collectLatest {
|
||||||
extensionsAdapter.updateData(extensions)
|
adapter.submitData(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lifecycleScope.launch {
|
|
||||||
combine(
|
|
||||||
animeExtensionManager.availableExtensionsFlow,
|
|
||||||
animeExtensionManager.installedExtensionsFlow
|
|
||||||
) { availableExtensions, installedExtensions ->
|
|
||||||
// Pair of available and installed extensions
|
|
||||||
Pair(availableExtensions, installedExtensions)
|
|
||||||
}.collect { pair ->
|
|
||||||
val (availableExtensions, installedExtensions) = pair
|
|
||||||
|
|
||||||
allExtensionsAdapter.updateData(availableExtensions, installedExtensions)
|
viewModel.invalidatePager() // Force a refresh of the pager
|
||||||
}
|
|
||||||
}
|
|
||||||
val extensionsRecyclerView: RecyclerView = binding.animeExtensionsRecyclerView
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateContentBasedOnQuery(query: String?) {
|
override fun updateContentBasedOnQuery(query: String?) {
|
||||||
if (query.isNullOrEmpty()) {
|
viewModel.setSearchQuery(query ?: "")
|
||||||
allExtensionsAdapter.filter("") // Reset the filter
|
}
|
||||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
|
||||||
extensionsRecyclerView.visibility = View.VISIBLE
|
override fun onInstallClick(pkg: AnimeExtension.Available) {
|
||||||
} else {
|
val context = requireContext()
|
||||||
allExtensionsAdapter.filter(query)
|
if (isAdded) {
|
||||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
val notificationManager =
|
||||||
extensionsRecyclerView.visibility = View.GONE
|
requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
// Start the installation process
|
||||||
|
animeExtensionManager.installExtension(pkg)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ installStep ->
|
||||||
|
val builder = NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||||
|
)
|
||||||
|
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||||
|
.setContentTitle("Installing extension")
|
||||||
|
.setContentText("Step: $installStep")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
notificationManager.notify(1, builder.build())
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(error)
|
||||||
|
val builder = NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||||
|
)
|
||||||
|
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||||
|
.setContentTitle("Installation 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(R.drawable.ic_round_download_24)
|
||||||
|
.setContentTitle("Installation complete")
|
||||||
|
.setContentText("The extension has been successfully installed.")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
notificationManager.notify(1, builder.build())
|
||||||
|
viewModel.invalidatePager()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,158 +123,4 @@ class AnimeExtensionsFragment : Fragment(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class AnimeExtensionsAdapter(
|
|
||||||
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
|
|
||||||
skipIcons: Boolean
|
|
||||||
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
|
|
||||||
DIFF_CALLBACK_INSTALLED
|
|
||||||
) {
|
|
||||||
|
|
||||||
val skipIcons = skipIcons
|
|
||||||
|
|
||||||
fun updateData(newExtensions: List<AnimeExtension.Installed>) {
|
|
||||||
submitList(newExtensions) // Use submitList instead of manual list handling
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_extension, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val extension = getItem(position) // Use getItem() from ListAdapter
|
|
||||||
holder.extensionNameTextView.text = extension.name
|
|
||||||
if (!skipIcons) {
|
|
||||||
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
|
||||||
}
|
|
||||||
if (extension.hasUpdate) {
|
|
||||||
holder.closeTextView.text = "Update"
|
|
||||||
holder.closeTextView.setTextColor(
|
|
||||||
ContextCompat.getColor(
|
|
||||||
holder.itemView.context,
|
|
||||||
R.color.warning
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
holder.closeTextView.text = "Uninstall"
|
|
||||||
}
|
|
||||||
holder.closeTextView.setOnClickListener {
|
|
||||||
onUninstallClicked(extension)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
|
||||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
|
||||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val DIFF_CALLBACK_INSTALLED =
|
|
||||||
object : DiffUtil.ItemCallback<AnimeExtension.Installed>() {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: AnimeExtension.Installed,
|
|
||||||
newItem: AnimeExtension.Installed
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.pkgName == newItem.pkgName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: AnimeExtension.Installed,
|
|
||||||
newItem: AnimeExtension.Installed
|
|
||||||
): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class AllAnimeExtensionsAdapter(
|
|
||||||
private val coroutineScope: CoroutineScope,
|
|
||||||
private val onButtonClicked: (AnimeExtension.Available) -> Unit,
|
|
||||||
skipIcons: Boolean
|
|
||||||
) : ListAdapter<AnimeExtension.Available, AllAnimeExtensionsAdapter.ViewHolder>(
|
|
||||||
DIFF_CALLBACK_AVAILABLE
|
|
||||||
) {
|
|
||||||
val skipIcons = skipIcons
|
|
||||||
|
|
||||||
fun updateData(
|
|
||||||
newExtensions: List<AnimeExtension.Available>,
|
|
||||||
installedExtensions: List<AnimeExtension.Installed> = emptyList()
|
|
||||||
) {
|
|
||||||
coroutineScope.launch(Dispatchers.Default) {
|
|
||||||
val installedPkgNames = installedExtensions.map { it.pkgName }.toSet()
|
|
||||||
val filteredExtensions = newExtensions.filter { it.pkgName !in installedPkgNames }
|
|
||||||
|
|
||||||
// Switch back to main thread to update UI
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
submitList(filteredExtensions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
): AllAnimeExtensionsAdapter.ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_extension_all, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val extension = getItem(position)
|
|
||||||
|
|
||||||
holder.extensionNameTextView.text = extension.name
|
|
||||||
|
|
||||||
if (!skipIcons) {
|
|
||||||
Glide.with(holder.itemView.context)
|
|
||||||
.load(extension.iconUrl)
|
|
||||||
.into(holder.extensionIconImageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.closeTextView.text = "Install"
|
|
||||||
holder.closeTextView.setOnClickListener {
|
|
||||||
onButtonClicked(extension)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filter(query: String) {
|
|
||||||
val filteredExtensions = if (query.isEmpty()) {
|
|
||||||
currentList
|
|
||||||
} else {
|
|
||||||
currentList.filter { it.name.contains(query, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
submitList(filteredExtensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
|
||||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
|
||||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val DIFF_CALLBACK_AVAILABLE =
|
|
||||||
object : DiffUtil.ItemCallback<AnimeExtension.Available>() {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: AnimeExtension.Available,
|
|
||||||
newItem: AnimeExtension.Available
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.pkgName == newItem.pkgName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: AnimeExtension.Available,
|
|
||||||
newItem: AnimeExtension.Available
|
|
||||||
): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,26 +13,8 @@ class DevelopersDialogFragment : BottomSheetDialogFragment() {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val developers = arrayOf(
|
private val developers = arrayOf(
|
||||||
Developer("vorobyovgabriel","https://avatars.githubusercontent.com/u/99561687?s=120&v=4","Owner","https://github.com/vorobyovgabriel"),
|
Developer("rebelonion","https://avatars.githubusercontent.com/u/87634197?v=4","Owner and Maintainer","https://github.com/rebelonion"),
|
||||||
Developer("brahmkshtriya","https://avatars.githubusercontent.com/u/69040506?s=120&v=4","Maintainer","https://github.com/brahmkshatriya"),
|
Developer("Wai What", "https://cdn.discordapp.com/avatars/928202695611908126/aeac4c867acbb8c3783356497055a426.webp?size=80", "Icon Designer", ""),
|
||||||
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")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
|||||||
@@ -1,54 +1,26 @@
|
|||||||
package ani.dantotsu.settings
|
package ani.dantotsu.settings
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
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.*
|
||||||
import android.os.Build.VERSION.*
|
import android.os.Build.VERSION.*
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.text.Editable
|
||||||
import android.view.View
|
import android.text.TextWatcher
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.AutoCompleteTextView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.SearchView
|
import android.widget.SearchView
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.Fragment
|
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.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
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.databinding.ActivityExtensionsBinding
|
||||||
import ani.dantotsu.home.AnimeFragment
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.home.MangaFragment
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionsActivity : AppCompatActivity() {
|
class ExtensionsActivity : AppCompatActivity() {
|
||||||
private val restartMainActivity = object : OnBackPressedCallback(false) {
|
private val restartMainActivity = object : OnBackPressedCallback(false) {
|
||||||
@@ -60,6 +32,7 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityExtensionsBinding.inflate(layoutInflater)
|
binding = ActivityExtensionsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
@@ -68,12 +41,14 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||||||
val viewPager = findViewById<ViewPager2>(R.id.viewPager)
|
val viewPager = findViewById<ViewPager2>(R.id.viewPager)
|
||||||
|
|
||||||
viewPager.adapter = object : FragmentStateAdapter(this) {
|
viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||||
override fun getItemCount(): Int = 2
|
override fun getItemCount(): Int = 4
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> AnimeExtensionsFragment()
|
0 -> InstalledAnimeExtensionsFragment()
|
||||||
1 -> MangaExtensionsFragment()
|
1 -> AnimeExtensionsFragment()
|
||||||
|
2 -> InstalledMangaExtensionsFragment()
|
||||||
|
3 -> MangaExtensionsFragment()
|
||||||
else -> AnimeExtensionsFragment()
|
else -> AnimeExtensionsFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,28 +56,29 @@ class ExtensionsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
tab.text = when (position) {
|
tab.text = when (position) {
|
||||||
0 -> "Anime" // Your tab title
|
0 -> "Installed Anime"
|
||||||
1 -> "Manga" // Your tab title
|
1 -> "Available Anime"
|
||||||
|
2 -> "Installed Manga"
|
||||||
|
3 -> "Available Manga"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
|
|
||||||
val searchView: SearchView = findViewById(R.id.searchView)
|
val searchView: AutoCompleteTextView = findViewById(R.id.searchViewText)
|
||||||
|
|
||||||
val extensionsHeader: LinearLayout = findViewById(R.id.extensionsHeader)
|
searchView.addTextChangedListener(object : TextWatcher {
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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}")
|
val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}")
|
||||||
if (currentFragment is SearchQueryHandler) {
|
if (currentFragment is SearchQueryHandler) {
|
||||||
currentFragment.updateContentBasedOnQuery(newText)
|
currentFragment.updateContentBasedOnQuery(s?.toString()?.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ani.dantotsu.R
|
|||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ActivityFaqBinding
|
import ani.dantotsu.databinding.ActivityFaqBinding
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class FAQActivity : AppCompatActivity() {
|
class FAQActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityFaqBinding
|
private lateinit var binding: ActivityFaqBinding
|
||||||
@@ -104,6 +105,7 @@ class FAQActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityFaqBinding.inflate(layoutInflater)
|
binding = ActivityFaqBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package ani.dantotsu.settings
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
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
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import 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
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
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 ->
|
||||||
|
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){
|
||||||
|
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 {
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 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,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
extensionsRecyclerView = binding.allAnimeExtensionsRecyclerView
|
||||||
|
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
extensionsRecyclerView.adapter = extensionsAdapter
|
||||||
|
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
animeExtensionManager.installedExtensionsFlow.collect { extensions ->
|
||||||
|
extensionsAdapter.updateData(extensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val extensionsRecyclerView: RecyclerView = binding.allAnimeExtensionsRecyclerView
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView();_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class AnimeExtensionsAdapter(
|
||||||
|
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
|
||||||
|
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
|
||||||
|
skipIcons: Boolean
|
||||||
|
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
|
||||||
|
DIFF_CALLBACK_INSTALLED
|
||||||
|
) {
|
||||||
|
|
||||||
|
val skipIcons = skipIcons
|
||||||
|
|
||||||
|
fun updateData(newExtensions: List<AnimeExtension.Installed>) {
|
||||||
|
submitList(newExtensions) // Use submitList instead of manual list handling
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_extension, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||||
|
holder.extensionNameTextView.text = extension.name
|
||||||
|
if (!skipIcons) {
|
||||||
|
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
||||||
|
}
|
||||||
|
if (extension.hasUpdate) {
|
||||||
|
holder.closeTextView.text = "Update"
|
||||||
|
holder.closeTextView.setTextColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
holder.itemView.context,
|
||||||
|
R.color.warning
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
holder.closeTextView.text = "Uninstall"
|
||||||
|
}
|
||||||
|
holder.closeTextView.setOnClickListener {
|
||||||
|
onUninstallClicked(extension)
|
||||||
|
}
|
||||||
|
holder.settingsImageView.setOnClickListener {
|
||||||
|
onSettingsClicked(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||||
|
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||||
|
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||||
|
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DIFF_CALLBACK_INSTALLED =
|
||||||
|
object : DiffUtil.ItemCallback<AnimeExtension.Installed>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: AnimeExtension.Installed,
|
||||||
|
newItem: AnimeExtension.Installed
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.pkgName == newItem.pkgName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: AnimeExtension.Installed,
|
||||||
|
newItem: AnimeExtension.Installed
|
||||||
|
): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package ani.dantotsu.settings
|
||||||
|
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
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
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import 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
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class InstalledMangaExtensionsFragment : Fragment() {
|
||||||
|
private var _binding: FragmentMangaExtensionsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private lateinit var extensionsRecyclerView: RecyclerView
|
||||||
|
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 =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
|
||||||
|
|
||||||
|
if (pkg.hasUpdate) {
|
||||||
|
mangaExtensionManager.updateExtension(pkg)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
||||||
|
.subscribe(
|
||||||
|
{ installStep ->
|
||||||
|
val builder = NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||||
|
)
|
||||||
|
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||||
|
.setContentTitle("Updating extension")
|
||||||
|
.setContentText("Step: $installStep")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
notificationManager.notify(1, builder.build())
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(error)
|
||||||
|
Log.e("MangaExtensionsAdapter", "Error: ", error) // Log the error
|
||||||
|
val builder = NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||||
|
)
|
||||||
|
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||||
|
.setContentTitle("Update failed: ${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 {
|
||||||
|
mangaExtensionManager.uninstallExtension(pkg.pkgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, skipIcons)
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
extensionsRecyclerView = binding.allMangaExtensionsRecyclerView
|
||||||
|
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
extensionsRecyclerView.adapter = extensionsAdapter
|
||||||
|
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
mangaExtensionManager.installedExtensionsFlow.collect { extensions ->
|
||||||
|
extensionsAdapter.updateData(extensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val extensionsRecyclerView: RecyclerView = binding.allMangaExtensionsRecyclerView
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView();_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MangaExtensionsAdapter(
|
||||||
|
private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
|
||||||
|
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
|
||||||
|
skipIcons: Boolean
|
||||||
|
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
|
||||||
|
DIFF_CALLBACK_INSTALLED
|
||||||
|
) {
|
||||||
|
|
||||||
|
val skipIcons = skipIcons
|
||||||
|
|
||||||
|
fun updateData(newExtensions: List<MangaExtension.Installed>) {
|
||||||
|
submitList(newExtensions) // Use submitList instead of manual list handling
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_extension, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||||
|
holder.extensionNameTextView.text = extension.name
|
||||||
|
if (!skipIcons) {
|
||||||
|
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
||||||
|
}
|
||||||
|
if (extension.hasUpdate) {
|
||||||
|
holder.closeTextView.text = "Update"
|
||||||
|
holder.closeTextView.setTextColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
holder.itemView.context,
|
||||||
|
R.color.warning
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
holder.closeTextView.text = "Uninstall"
|
||||||
|
}
|
||||||
|
holder.closeTextView.setOnClickListener {
|
||||||
|
onUninstallClicked(extension)
|
||||||
|
}
|
||||||
|
holder.settingsImageView.setOnClickListener {
|
||||||
|
onSettingsClicked(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
||||||
|
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||||
|
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||||
|
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DIFF_CALLBACK_INSTALLED =
|
||||||
|
object : DiffUtil.ItemCallback<MangaExtension.Installed>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: MangaExtension.Installed,
|
||||||
|
newItem: MangaExtension.Installed
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.pkgName == newItem.pkgName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: MangaExtension.Installed,
|
||||||
|
newItem: MangaExtension.Installed
|
||||||
|
): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,113 +2,89 @@ package ani.dantotsu.settings
|
|||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.paging.PagingData
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
|
||||||
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
||||||
import ani.dantotsu.loadData
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
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(),
|
class MangaExtensionsFragment : Fragment(),
|
||||||
SearchQueryHandler {
|
SearchQueryHandler, OnMangaInstallClickListener {
|
||||||
private var _binding: FragmentMangaExtensionsBinding? = null
|
private var _binding: FragmentMangaExtensionsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
private val viewModel: MangaExtensionsViewModel by viewModels {
|
||||||
|
MangaExtensionsViewModelFactory(mangaExtensionManager)
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var extensionsRecyclerView: RecyclerView
|
private val adapter by lazy {
|
||||||
private lateinit var allextenstionsRecyclerView: RecyclerView
|
MangaExtensionAdapter(this)
|
||||||
private val mangaExtensionManager: MangaExtensionManager = Injekt.get<MangaExtensionManager>()
|
}
|
||||||
private val extensionsAdapter = MangaExtensionsAdapter({ pkg ->
|
|
||||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
|
||||||
val context = requireContext() // Store context in a variable
|
|
||||||
val notificationManager =
|
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
|
|
||||||
|
|
||||||
if (pkg.hasUpdate) {
|
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
|
||||||
mangaExtensionManager.updateExtension(pkg)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
|
||||||
.subscribe(
|
override fun onCreateView(
|
||||||
{ installStep ->
|
inflater: LayoutInflater,
|
||||||
val builder = NotificationCompat.Builder(
|
container: ViewGroup?,
|
||||||
context,
|
savedInstanceState: Bundle?
|
||||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
): View {
|
||||||
)
|
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
|
||||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
|
||||||
.setContentTitle("Updating extension")
|
binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = false
|
||||||
.setContentText("Step: $installStep")
|
binding.allMangaExtensionsRecyclerView.adapter = adapter
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
binding.allMangaExtensionsRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
notificationManager.notify(1, builder.build())
|
(binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true
|
||||||
},
|
|
||||||
{ error ->
|
lifecycleScope.launch {
|
||||||
Log.e("MangaExtensionsAdapter", "Error: ", error) // Log the error
|
viewModel.pagerFlow.collectLatest { pagingData ->
|
||||||
val builder = NotificationCompat.Builder(
|
adapter.submitData(pagingData)
|
||||||
context,
|
|
||||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
|
||||||
)
|
|
||||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
|
||||||
.setContentTitle("Update failed")
|
|
||||||
.setContentText("Error: ${error.message}")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
notificationManager.notify(1, builder.build())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
val builder = NotificationCompat.Builder(
|
|
||||||
context,
|
|
||||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
|
||||||
)
|
|
||||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
|
||||||
.setContentTitle("Update complete")
|
|
||||||
.setContentText("The extension has been successfully updated.")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
||||||
notificationManager.notify(1, builder.build())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
mangaExtensionManager.uninstallExtension(pkg.pkgName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, skipIcons)
|
|
||||||
|
|
||||||
private val allExtensionsAdapter =
|
viewModel.invalidatePager() // Force a refresh of the pager
|
||||||
AllMangaExtensionsAdapter(lifecycleScope, { pkgName ->
|
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateContentBasedOnQuery(query: String?) {
|
||||||
|
viewModel.setSearchQuery(query ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInstallClick(pkg: MangaExtension.Available) {
|
||||||
|
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||||
|
val context = requireContext()
|
||||||
val notificationManager =
|
val notificationManager =
|
||||||
requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
// Start the installation process
|
// Start the installation process
|
||||||
mangaExtensionManager.installExtension(pkgName)
|
mangaExtensionManager.installExtension(pkg)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ installStep ->
|
{ installStep ->
|
||||||
val builder = NotificationCompat.Builder(
|
val builder = NotificationCompat.Builder(
|
||||||
requireContext(),
|
context,
|
||||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||||
)
|
)
|
||||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||||
@@ -118,75 +94,30 @@ class MangaExtensionsFragment : Fragment(),
|
|||||||
notificationManager.notify(1, builder.build())
|
notificationManager.notify(1, builder.build())
|
||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(error)
|
||||||
val builder = NotificationCompat.Builder(
|
val builder = NotificationCompat.Builder(
|
||||||
requireContext(),
|
context,
|
||||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||||
)
|
)
|
||||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||||
.setContentTitle("Installation failed")
|
.setContentTitle("Installation failed: ${error.message}")
|
||||||
.setContentText("Error: ${error.message}")
|
.setContentText("Error: ${error.message}")
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
notificationManager.notify(1, builder.build())
|
notificationManager.notify(1, builder.build())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
val builder = NotificationCompat.Builder(
|
val builder = NotificationCompat.Builder(
|
||||||
requireContext(),
|
context,
|
||||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||||
)
|
)
|
||||||
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
|
.setSmallIcon(R.drawable.ic_round_download_24)
|
||||||
.setContentTitle("Installation complete")
|
.setContentTitle("Installation complete")
|
||||||
.setContentText("The extension has been successfully installed.")
|
.setContentText("The extension has been successfully installed.")
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
notificationManager.notify(1, builder.build())
|
notificationManager.notify(1, builder.build())
|
||||||
|
viewModel.invalidatePager()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}, skipIcons)
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
extensionsRecyclerView = binding.mangaExtensionsRecyclerView
|
|
||||||
extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
extensionsRecyclerView.adapter = extensionsAdapter
|
|
||||||
|
|
||||||
allextenstionsRecyclerView = binding.allMangaExtensionsRecyclerView
|
|
||||||
allextenstionsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
allextenstionsRecyclerView.adapter = allExtensionsAdapter
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
mangaExtensionManager.installedExtensionsFlow.collect { extensions ->
|
|
||||||
extensionsAdapter.updateData(extensions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
combine(
|
|
||||||
mangaExtensionManager.availableExtensionsFlow,
|
|
||||||
mangaExtensionManager.installedExtensionsFlow
|
|
||||||
) { availableExtensions, installedExtensions ->
|
|
||||||
// Pair of available and installed extensions
|
|
||||||
Pair(availableExtensions, installedExtensions)
|
|
||||||
}.collect { pair ->
|
|
||||||
val (availableExtensions, installedExtensions) = pair
|
|
||||||
allExtensionsAdapter.updateData(availableExtensions, installedExtensions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val extensionsRecyclerView: RecyclerView = binding.mangaExtensionsRecyclerView
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateContentBasedOnQuery(query: String?) {
|
|
||||||
if (query.isNullOrEmpty()) {
|
|
||||||
allExtensionsAdapter.filter("") // Reset the filter
|
|
||||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
|
||||||
extensionsRecyclerView.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
allExtensionsAdapter.filter(query)
|
|
||||||
allextenstionsRecyclerView.visibility = View.VISIBLE
|
|
||||||
extensionsRecyclerView.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,165 +125,6 @@ class MangaExtensionsFragment : Fragment(),
|
|||||||
super.onDestroyView();_binding = null
|
super.onDestroyView();_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MangaExtensionsAdapter(
|
|
||||||
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
|
|
||||||
skipIcons: Boolean
|
|
||||||
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
|
|
||||||
DIFF_CALLBACK_INSTALLED
|
|
||||||
) {
|
|
||||||
|
|
||||||
val skipIcons = skipIcons
|
|
||||||
|
|
||||||
// Use submitList to update data
|
|
||||||
fun updateData(newExtensions: List<MangaExtension.Installed>) {
|
|
||||||
submitList(newExtensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_extension, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val extension = getItem(position) // Use getItem from ListAdapter
|
|
||||||
|
|
||||||
holder.extensionNameTextView.text = extension.name
|
|
||||||
if (!skipIcons) {
|
|
||||||
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension.hasUpdate) {
|
|
||||||
holder.closeTextView.text = "Update"
|
|
||||||
holder.closeTextView.setTextColor(
|
|
||||||
ContextCompat.getColor(
|
|
||||||
holder.itemView.context,
|
|
||||||
R.color.warning
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
holder.closeTextView.text = "Uninstall"
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.closeTextView.setOnClickListener {
|
|
||||||
onUninstallClicked(extension)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
|
||||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
|
||||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val DIFF_CALLBACK_INSTALLED =
|
|
||||||
object : DiffUtil.ItemCallback<MangaExtension.Installed>() {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: MangaExtension.Installed,
|
|
||||||
newItem: MangaExtension.Installed
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.pkgName == newItem.pkgName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: MangaExtension.Installed,
|
|
||||||
newItem: MangaExtension.Installed
|
|
||||||
): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class AllMangaExtensionsAdapter(
|
|
||||||
private val coroutineScope: CoroutineScope,
|
|
||||||
private val onButtonClicked: (MangaExtension.Available) -> Unit,
|
|
||||||
skipIcons: Boolean
|
|
||||||
) : ListAdapter<MangaExtension.Available, AllMangaExtensionsAdapter.ViewHolder>(
|
|
||||||
DIFF_CALLBACK_AVAILABLE
|
|
||||||
) {
|
|
||||||
init {
|
|
||||||
setHasStableIds(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val skipIcons = skipIcons
|
|
||||||
|
|
||||||
// Use submitList to update the data
|
|
||||||
fun updateData(
|
|
||||||
newExtensions: List<MangaExtension.Available>,
|
|
||||||
installedExtensions: List<MangaExtension.Installed> = emptyList()
|
|
||||||
) {
|
|
||||||
coroutineScope.launch(Dispatchers.Default) {
|
|
||||||
val installedPkgNames = installedExtensions.map { it.pkgName }.toSet()
|
|
||||||
val filteredExtensions = newExtensions.filter { it.pkgName !in installedPkgNames }
|
|
||||||
|
|
||||||
// Switch back to main thread to update UI
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
submitList(filteredExtensions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_extension_all, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val extension = getItem(position) // Use getItem from ListAdapter
|
|
||||||
|
|
||||||
holder.extensionNameTextView.text = extension.name
|
|
||||||
if (!skipIcons) {
|
|
||||||
Glide.with(holder.itemView.context)
|
|
||||||
.load(extension.iconUrl)
|
|
||||||
.into(holder.extensionIconImageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.closeTextView.text = "Install"
|
|
||||||
holder.closeTextView.setOnClickListener {
|
|
||||||
onButtonClicked(extension)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtering function
|
|
||||||
fun filter(query: String) {
|
|
||||||
val filteredExtensions = if (query.isEmpty()) {
|
|
||||||
currentList
|
|
||||||
} else {
|
|
||||||
currentList.filter { it.name.contains(query, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
submitList(filteredExtensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
|
||||||
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
|
||||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val DIFF_CALLBACK_AVAILABLE =
|
|
||||||
object : DiffUtil.ItemCallback<MangaExtension.Available>() {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: MangaExtension.Available,
|
|
||||||
newItem: MangaExtension.Available
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.pkgName == newItem.pkgName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: MangaExtension.Available,
|
|
||||||
newItem: MangaExtension.Available
|
|
||||||
): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ import ani.dantotsu.databinding.ActivityPlayerSettingsBinding
|
|||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ class PlayerSettingsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityPlayerSettingsBinding.inflate(layoutInflater)
|
binding = ActivityPlayerSettingsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import ani.dantotsu.navBarHeight
|
|||||||
import ani.dantotsu.saveData
|
import ani.dantotsu.saveData
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
|
||||||
class ReaderSettingsActivity : AppCompatActivity() {
|
class ReaderSettingsActivity : AppCompatActivity() {
|
||||||
lateinit var binding: ActivityReaderSettingsBinding
|
lateinit var binding: ActivityReaderSettingsBinding
|
||||||
private val reader = "reader_settings"
|
private val reader = "reader_settings"
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityReaderSettingsBinding.inflate(layoutInflater)
|
binding = ActivityReaderSettingsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -31,6 +32,8 @@ import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
|
|||||||
import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
|
import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||||
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
|
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
@@ -54,6 +57,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
@@ -98,6 +102,26 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
|||||||
binding.settingsUseMaterialYou.isChecked = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false)
|
binding.settingsUseMaterialYou.isChecked = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false)
|
||||||
binding.settingsUseMaterialYou.setOnCheckedChangeListener { _, isChecked ->
|
binding.settingsUseMaterialYou.setOnCheckedChangeListener { _, isChecked ->
|
||||||
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_material_you", isChecked).apply()
|
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_material_you", isChecked).apply()
|
||||||
|
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())
|
||||||
|
|
||||||
|
binding.themeSwitcher.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, ThemeManager.Companion.Theme.values().map { it.theme.substring(0, 1) + it.theme.substring(1).lowercase() }))
|
||||||
|
|
||||||
|
binding.themeSwitcher.setOnItemClickListener { _, _, i, _ ->
|
||||||
|
getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putString("theme", ThemeManager.Companion.Theme.values()[i].theme).apply()
|
||||||
|
//ActivityHelper.shouldRefreshMainActivity = true
|
||||||
|
binding.themeSwitcher.clearFocus()
|
||||||
|
restartApp()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//val animeSource = loadData<Int>("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0
|
//val animeSource = loadData<Int>("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0
|
||||||
@@ -150,6 +174,15 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan")
|
||||||
|
binding.settingsExtensionDns.setText(exDns[networkPreferences.dohProvider().get()], false)
|
||||||
|
binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns))
|
||||||
|
binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
|
||||||
|
networkPreferences.dohProvider().set(i)
|
||||||
|
binding.settingsExtensionDns.clearFocus()
|
||||||
|
Toast.makeText(this, "Restart app to apply changes", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
binding.settingsDownloadInSd.isChecked = loadData("sd_dl") ?: false
|
binding.settingsDownloadInSd.isChecked = loadData("sd_dl") ?: false
|
||||||
binding.settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked ->
|
binding.settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked ->
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
@@ -381,7 +414,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var curTime = loadData<Int>("subscriptions_time") ?: defaultTime
|
var curTime = loadData<Int>("subscriptions_time_s") ?: defaultTime
|
||||||
val timeNames = timeMinutes.map {
|
val timeNames = timeMinutes.map {
|
||||||
val mins = it % 60
|
val mins = it % 60
|
||||||
val hours = it / 60
|
val hours = it / 60
|
||||||
@@ -394,7 +427,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
|||||||
speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i ->
|
speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i ->
|
||||||
curTime = i
|
curTime = i
|
||||||
binding.settingsSubscriptionsTime.text = getString(R.string.subscriptions_checking_time_s, timeNames[i])
|
binding.settingsSubscriptionsTime.text = getString(R.string.subscriptions_checking_time_s, timeNames[i])
|
||||||
saveData("subscriptions_time", curTime)
|
saveData("subscriptions_time_s", curTime)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
startSubscription(true)
|
startSubscription(true)
|
||||||
}.show()
|
}.show()
|
||||||
@@ -538,7 +571,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
|||||||
title = "Enjoying the App?"
|
title = "Enjoying the App?"
|
||||||
addView(TextView(this@SettingsActivity).apply {
|
addView(TextView(this@SettingsActivity).apply {
|
||||||
text =
|
text =
|
||||||
"Consider donating!r"
|
"Consider donating!"
|
||||||
})
|
})
|
||||||
|
|
||||||
setNegativeButton("no moners :(") {
|
setNegativeButton("no moners :(") {
|
||||||
@@ -557,4 +590,18 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun restartApp() {
|
||||||
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
R.string.restart_app, Snackbar.LENGTH_SHORT
|
||||||
|
).apply {
|
||||||
|
val mainIntent =
|
||||||
|
Intent.makeRestartActivityTask(context.packageManager.getLaunchIntentForPackage(context.packageName)!!.component)
|
||||||
|
setAction("Do it!") {
|
||||||
|
context.startActivity(mainIntent)
|
||||||
|
Runtime.getRuntime().exit(0)
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,10 @@ package ani.dantotsu.settings
|
|||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -26,6 +28,12 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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) {
|
if (Anilist.token != null) {
|
||||||
binding.settingsLogin.setText(R.string.logout)
|
binding.settingsLogin.setText(R.string.logout)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.ActivityUserInterfaceSettingsBinding
|
import ani.dantotsu.databinding.ActivityUserInterfaceSettingsBinding
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
|
||||||
class UserInterfaceSettingsActivity : AppCompatActivity() {
|
class UserInterfaceSettingsActivity : AppCompatActivity() {
|
||||||
@@ -15,6 +16,7 @@ class UserInterfaceSettingsActivity : AppCompatActivity() {
|
|||||||
private val ui = "ui_settings"
|
private val ui = "ui_settings"
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
binding = ActivityUserInterfaceSettingsBinding.inflate(layoutInflater)
|
binding = ActivityUserInterfaceSettingsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package ani.dantotsu.settings.paging
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import ani.dantotsu.databinding.ItemExtensionAllBinding
|
||||||
|
import ani.dantotsu.loadData
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
|
||||||
|
|
||||||
|
class AnimeExtensionsViewModelFactory(
|
||||||
|
private val animeExtensionManager: AnimeExtensionManager
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return AnimeExtensionsViewModel(animeExtensionManager) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AnimeExtensionsViewModel(
|
||||||
|
private val animeExtensionManager: AnimeExtensionManager
|
||||||
|
) : ViewModel() {
|
||||||
|
private val searchQuery = MutableStateFlow("")
|
||||||
|
private var currentPagingSource: AnimeExtensionPagingSource? = null
|
||||||
|
fun setSearchQuery(query: String) {
|
||||||
|
searchQuery.value = query
|
||||||
|
}
|
||||||
|
fun invalidatePager() {
|
||||||
|
currentPagingSource?.invalidate()
|
||||||
|
}
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val pagerFlow: Flow<PagingData<AnimeExtension.Available>> = searchQuery.flatMapLatest { query ->
|
||||||
|
Pager(
|
||||||
|
PagingConfig(
|
||||||
|
pageSize = 15,
|
||||||
|
initialLoadSize = 15,
|
||||||
|
prefetchDistance = 15
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AnimeExtensionPagingSource(
|
||||||
|
animeExtensionManager.availableExtensionsFlow,
|
||||||
|
animeExtensionManager.installedExtensionsFlow,
|
||||||
|
searchQuery
|
||||||
|
).also { currentPagingSource = it }
|
||||||
|
}.flow
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimeExtensionPagingSource(
|
||||||
|
private val availableExtensionsFlow: StateFlow<List<AnimeExtension.Available>>,
|
||||||
|
private val installedExtensionsFlow: StateFlow<List<AnimeExtension.Installed>>,
|
||||||
|
private val searchQuery: StateFlow<String>
|
||||||
|
) : PagingSource<Int, AnimeExtension.Available>() {
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AnimeExtension.Available> {
|
||||||
|
val position = params.key ?: 0
|
||||||
|
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
|
||||||
|
val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
|
||||||
|
val query = searchQuery.first()
|
||||||
|
val filteredExtensions = if (query.isEmpty()) {
|
||||||
|
availableExtensions
|
||||||
|
} else {
|
||||||
|
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val sublist = filteredExtensions.subList(
|
||||||
|
fromIndex = position,
|
||||||
|
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.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
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, AnimeExtension.Available>): Int? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListener) :
|
||||||
|
PagingDataAdapter<AnimeExtension.Available, AnimeExtensionAdapter.AnimeExtensionViewHolder>(
|
||||||
|
DIFF_CALLBACK
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val skipIcons = loadData("skip_extension_icons") ?: false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<AnimeExtension.Available>() {
|
||||||
|
override fun areItemsTheSame(oldItem: AnimeExtension.Available, newItem: AnimeExtension.Available): Boolean {
|
||||||
|
// Your logic here
|
||||||
|
return oldItem.pkgName == newItem.pkgName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: AnimeExtension.Available, newItem: AnimeExtension.Available): Boolean {
|
||||||
|
// Your logic here
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimeExtensionViewHolder {
|
||||||
|
val binding = ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return AnimeExtensionViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: AnimeExtensionViewHolder, position: Int) {
|
||||||
|
val extension = getItem(position)
|
||||||
|
if (extension != null) {
|
||||||
|
if (!skipIcons) {
|
||||||
|
Glide.with(holder.itemView.context)
|
||||||
|
.load(extension.iconUrl)
|
||||||
|
.into(holder.extensionIconImageView)
|
||||||
|
}
|
||||||
|
holder.bind(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class AnimeExtensionViewHolder(private val binding: ItemExtensionAllBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
|
binding.closeTextView.setOnClickListener {
|
||||||
|
val extension = getItem(bindingAdapterPosition)
|
||||||
|
if (extension != null) {
|
||||||
|
clickListener.onInstallClick(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val extensionIconImageView: ImageView = binding.extensionIconImageView
|
||||||
|
fun bind(extension: AnimeExtension.Available) {
|
||||||
|
binding.extensionNameTextView.text = extension.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnAnimeInstallClickListener {
|
||||||
|
fun onInstallClick(pkg: AnimeExtension.Available)
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package ani.dantotsu.settings.paging
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import ani.dantotsu.databinding.ItemExtensionAllBinding
|
||||||
|
import ani.dantotsu.loadData
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import java.lang.Math.min
|
||||||
|
|
||||||
|
class MangaExtensionsViewModelFactory(
|
||||||
|
private val mangaExtensionManager: MangaExtensionManager
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return MangaExtensionsViewModel(mangaExtensionManager) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaExtensionsViewModel(
|
||||||
|
private val mangaExtensionManager: MangaExtensionManager
|
||||||
|
) : ViewModel() {
|
||||||
|
private val searchQuery = MutableStateFlow("")
|
||||||
|
private var currentPagingSource: MangaExtensionPagingSource? = null
|
||||||
|
|
||||||
|
fun setSearchQuery(query: String) {
|
||||||
|
searchQuery.value = query
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidatePager() {
|
||||||
|
currentPagingSource?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val pagerFlow: Flow<PagingData<MangaExtension.Available>> = searchQuery.flatMapLatest { query ->
|
||||||
|
Pager(
|
||||||
|
PagingConfig(
|
||||||
|
pageSize = 15,
|
||||||
|
initialLoadSize = 15,
|
||||||
|
prefetchDistance = 15
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
MangaExtensionPagingSource(
|
||||||
|
mangaExtensionManager.availableExtensionsFlow,
|
||||||
|
mangaExtensionManager.installedExtensionsFlow,
|
||||||
|
searchQuery
|
||||||
|
).also { currentPagingSource = it }
|
||||||
|
}.flow
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MangaExtensionPagingSource(
|
||||||
|
private val availableExtensionsFlow: StateFlow<List<MangaExtension.Available>>,
|
||||||
|
private val installedExtensionsFlow: StateFlow<List<MangaExtension.Installed>>,
|
||||||
|
private val searchQuery: StateFlow<String>
|
||||||
|
) : PagingSource<Int, MangaExtension.Available>() {
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MangaExtension.Available> {
|
||||||
|
val position = params.key ?: 0
|
||||||
|
val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet()
|
||||||
|
val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions }
|
||||||
|
val query = searchQuery.first()
|
||||||
|
val filteredExtensions = if (query.isEmpty()) {
|
||||||
|
availableExtensions
|
||||||
|
} else {
|
||||||
|
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val sublist = filteredExtensions.subList(
|
||||||
|
fromIndex = position,
|
||||||
|
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.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
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, MangaExtension.Available>): Int? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListener) :
|
||||||
|
PagingDataAdapter<MangaExtension.Available, MangaExtensionAdapter.MangaExtensionViewHolder>(
|
||||||
|
DIFF_CALLBACK
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val skipIcons = loadData("skip_extension_icons") ?: false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MangaExtension.Available>() {
|
||||||
|
override fun areItemsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean {
|
||||||
|
return oldItem.pkgName == newItem.pkgName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaExtensionViewHolder {
|
||||||
|
val binding = ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return MangaExtensionViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MangaExtensionViewHolder, position: Int) {
|
||||||
|
val extension = getItem(position)
|
||||||
|
if (extension != null) {
|
||||||
|
if (!skipIcons) {
|
||||||
|
Glide.with(holder.itemView.context)
|
||||||
|
.load(extension.iconUrl)
|
||||||
|
.into(holder.extensionIconImageView)
|
||||||
|
}
|
||||||
|
holder.bind(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MangaExtensionViewHolder(private val binding: ItemExtensionAllBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
|
binding.closeTextView.setOnClickListener {
|
||||||
|
val extension = getItem(bindingAdapterPosition)
|
||||||
|
if (extension != null) {
|
||||||
|
clickListener.onInstallClick(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val extensionIconImageView: ImageView = binding.extensionIconImageView
|
||||||
|
fun bind(extension: MangaExtension.Available) {
|
||||||
|
binding.extensionNameTextView.text = extension.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnMangaInstallClickListener {
|
||||||
|
fun onInstallClick(pkg: MangaExtension.Available)
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ class AlarmReceiver : BroadcastReceiver() {
|
|||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
val curTime = loadData<Int>("subscriptions_time", context) ?: defaultTime
|
val curTime = loadData<Int>("subscriptions_time_s", context) ?: defaultTime
|
||||||
|
|
||||||
if (timeMinutes[curTime] > 0)
|
if (timeMinutes[curTime] > 0)
|
||||||
alarmManager.setRepeating(
|
alarmManager.setRepeating(
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import kotlinx.coroutines.launch
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
class Subscription {
|
class Subscription {
|
||||||
companion object {
|
companion object {
|
||||||
const val defaultTime = 8
|
const val defaultTime = 1
|
||||||
val timeMinutes = arrayOf(0L, 5, 10, 15, 30, 45, 60, 90, 120, 180, 240, 360, 480, 720, 1440)
|
val timeMinutes = arrayOf(0L, 720, 1440)
|
||||||
|
|
||||||
private var alreadyStarted = false
|
private var alreadyStarted = false
|
||||||
fun Context.startSubscription(force: Boolean = false) {
|
fun Context.startSubscription(force: Boolean = false) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ani.dantotsu.parsers.*
|
|||||||
import ani.dantotsu.saveData
|
import ani.dantotsu.saveData
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.media.manga.MangaNameAdapter
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
class SubscriptionHelper {
|
class SubscriptionHelper {
|
||||||
@@ -76,7 +77,7 @@ class SubscriptionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return chp?.apply {
|
return chp?.apply {
|
||||||
selected.latest = number.toFloat()
|
selected.latest = MangaNameAdapter.findChapterNumber(number) ?: 0f
|
||||||
saveSelected(context, id, selected)
|
saveSelected(context, id, selected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class SubscriptionWorker(val context: Context, params: WorkerParameters) : Corou
|
|||||||
|
|
||||||
private const val SUBSCRIPTION_WORK_NAME = "work_subscription"
|
private const val SUBSCRIPTION_WORK_NAME = "work_subscription"
|
||||||
fun enqueue(context: Context) {
|
fun enqueue(context: Context) {
|
||||||
val curTime = loadData<Int>("subscriptions_time") ?: defaultTime
|
val curTime = loadData<Int>("subscriptions_time_s") ?: defaultTime
|
||||||
if(timeMinutes[curTime]>0L) {
|
if(timeMinutes[curTime]>0L) {
|
||||||
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
||||||
val periodicSyncDataWork = PeriodicWorkRequest.Builder(
|
val periodicSyncDataWork = PeriodicWorkRequest.Builder(
|
||||||
|
|||||||
56
app/src/main/java/ani/dantotsu/themes/ThemeManager.kt
Normal file
56
app/src/main/java/ani/dantotsu/themes/ThemeManager.kt
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
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"),
|
||||||
|
BLUE("BLUE"),
|
||||||
|
GREEN("GREEN"),
|
||||||
|
PINK("PINK"),
|
||||||
|
RED("RED"),
|
||||||
|
LAVENDER("LAVENDER"),
|
||||||
|
MONOCHROME("MONOCHROME (BETA)");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(value: String): Theme {
|
||||||
|
return values().find { it.theme == value } ?: PURPLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -6,20 +6,26 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import rx.subjects.Subject
|
import rx.subjects.Subject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.ObjectInputStream
|
||||||
|
import java.io.ObjectOutputStream
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
data class Track(val url: String, val lang: String)
|
data class Track(val url: String, val lang: String) : Serializable
|
||||||
|
|
||||||
open class Video(
|
open class Video(
|
||||||
val url: String = "",
|
val url: String = "",
|
||||||
val quality: String = "",
|
val quality: String = "",
|
||||||
var videoUrl: String? = null,
|
var videoUrl: String? = null,
|
||||||
val headers: Headers? = null,
|
headers: Headers? = null,
|
||||||
// "url", "language-label-2", "url2", "language-label-2"
|
// "url", "language-label-2", "url2", "language-label-2"
|
||||||
val subtitleTracks: List<Track> = emptyList(),
|
val subtitleTracks: List<Track> = emptyList(),
|
||||||
val audioTracks: List<Track> = emptyList(),
|
val audioTracks: List<Track> = emptyList(),
|
||||||
) : Serializable, ProgressListener {
|
) : Serializable, ProgressListener {
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var headers: Headers? = headers
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
constructor(
|
constructor(
|
||||||
url: String,
|
url: String,
|
||||||
@@ -90,4 +96,27 @@ open class Video(
|
|||||||
READY,
|
READY,
|
||||||
ERROR,
|
ERROR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun writeObject(out: ObjectOutputStream) {
|
||||||
|
out.defaultWriteObject()
|
||||||
|
val headersMap: Map<String, List<String>> = headers?.toMultimap() ?: emptyMap()
|
||||||
|
out.writeObject(headersMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, ClassNotFoundException::class)
|
||||||
|
private fun readObject(input: ObjectInputStream) {
|
||||||
|
input.defaultReadObject()
|
||||||
|
val headersMap = input.readObject() as? Map<String, List<String>>
|
||||||
|
headers = headersMap?.let { map ->
|
||||||
|
val builder = Headers.Builder()
|
||||||
|
for ((key, values) in map) {
|
||||||
|
for (value in values) {
|
||||||
|
builder.add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.anime.util
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||||
@@ -24,6 +25,7 @@ class AnimeExtensionInstallActivity : Activity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||||
.setDataAndType(intent.data, intent.type)
|
.setDataAndType(intent.data, intent.type)
|
||||||
|
|||||||
@@ -108,8 +108,11 @@ internal class AnimeExtensionInstaller(private val context: Context) {
|
|||||||
// Get the current download status
|
// Get the current download status
|
||||||
.map {
|
.map {
|
||||||
downloadManager.query(query).use { cursor ->
|
downloadManager.query(query).use { cursor ->
|
||||||
cursor.moveToFirst()
|
if (cursor.moveToFirst()) {
|
||||||
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
||||||
|
} else {
|
||||||
|
DownloadManager.STATUS_FAILED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ignore duplicate results
|
// Ignore duplicate results
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.manga.util
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||||
@@ -24,6 +25,7 @@ class MangaExtensionInstallActivity : Activity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||||
.setDataAndType(intent.data, intent.type)
|
.setDataAndType(intent.data, intent.type)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class NetworkPreferences(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun dohProvider(): Preference<Int> {
|
fun dohProvider(): Preference<Int> {
|
||||||
return preferenceStore.getInt("doh_provider", 1)
|
return preferenceStore.getInt("doh_provider", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultUserAgent(): Preference<String> {
|
fun defaultUserAgent(): Preference<String> {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.manga
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.MangaSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
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.manga.model.MangaSourceData
|
||||||
|
import tachiyomi.domain.source.manga.model.StubMangaSource
|
||||||
|
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||||
|
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class AndroidMangaSourceManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val extensionManager: MangaExtensionManager,
|
||||||
|
) : MangaSourceManager {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
|
||||||
|
private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, MangaSource>())
|
||||||
|
|
||||||
|
private val stubSourcesMap = ConcurrentHashMap<Long, StubMangaSource>()
|
||||||
|
|
||||||
|
override val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
extensionManager.installedExtensionsFlow
|
||||||
|
.collectLatest { extensions ->
|
||||||
|
val mutableMap = ConcurrentHashMap<Long, MangaSource>(
|
||||||
|
mapOf(
|
||||||
|
LocalMangaSource.ID to LocalMangaSource(
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
extensions.forEach { extension ->
|
||||||
|
extension.sources.forEach {
|
||||||
|
mutableMap[it.id] = it
|
||||||
|
registerStubSource(it.toSourceData())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourcesMapFlow.value = mutableMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(sourceKey: Long): MangaSource? {
|
||||||
|
return sourcesMapFlow.value[sourceKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOrStub(sourceKey: Long): MangaSource {
|
||||||
|
return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
||||||
|
runBlocking { createStubSource(sourceKey) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>()
|
||||||
|
|
||||||
|
override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance<CatalogueSource>()
|
||||||
|
|
||||||
|
override fun getStubSources(): List<StubMangaSource> {
|
||||||
|
val onlineSourceIds = getOnlineSources().map { it.id }
|
||||||
|
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerStubSource(sourceData: MangaSourceData) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createStubSource(id: Long): StubMangaSource {
|
||||||
|
return StubMangaSource(MangaSourceData(id, "", ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.manga
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.source.MangaSource
|
||||||
|
import tachiyomi.domain.source.manga.model.MangaSourceData
|
||||||
|
import tachiyomi.domain.source.manga.model.StubMangaSource
|
||||||
|
import tachiyomi.source.local.entries.manga.isLocal
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
fun MangaSource.icon(): Drawable? = Injekt.get<MangaExtensionManager>().getAppIconForSource(this.id)
|
||||||
|
|
||||||
|
fun MangaSource.getPreferenceKey(): String = "source_$id"
|
||||||
|
|
||||||
|
fun MangaSource.toSourceData(): MangaSourceData = MangaSourceData(id = id, lang = lang, name = name)
|
||||||
|
|
||||||
|
fun MangaSource.getNameForMangaInfo(): 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 MangaSource.isLocalOrStub(): Boolean = isLocal() || this is StubMangaSource
|
||||||
117
app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
Normal file
117
app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package eu.kanade.tachiyomi.util.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.StatFs
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object DiskUtil {
|
||||||
|
|
||||||
|
fun hashKeyForDisk(key: String): String {
|
||||||
|
return Hash.md5(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDirectorySize(f: File): Long {
|
||||||
|
var size: Long = 0
|
||||||
|
if (f.isDirectory) {
|
||||||
|
for (file in f.listFiles().orEmpty()) {
|
||||||
|
size += getDirectorySize(file)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size = f.length()
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the available space for the disk that a file path points to, in bytes.
|
||||||
|
*/
|
||||||
|
fun getAvailableStorageSpace(f: UniFile): Long {
|
||||||
|
return try {
|
||||||
|
val stat = StatFs(f.uri.path)
|
||||||
|
stat.availableBlocksLong * stat.blockSizeLong
|
||||||
|
} catch (_: Exception) {
|
||||||
|
-1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root folders of all the available external storages.
|
||||||
|
*/
|
||||||
|
fun getExternalStorages(context: Context): List<File> {
|
||||||
|
return ContextCompat.getExternalFilesDirs(context, null)
|
||||||
|
.filterNotNull()
|
||||||
|
.mapNotNull {
|
||||||
|
val file = File(it.absolutePath.substringBefore("/Android/"))
|
||||||
|
val state = Environment.getExternalStorageState(file)
|
||||||
|
if (state == Environment.MEDIA_MOUNTED || state == Environment.MEDIA_MOUNTED_READ_ONLY) {
|
||||||
|
file
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't display downloaded chapters in gallery apps creating `.nomedia`.
|
||||||
|
*/
|
||||||
|
fun createNoMediaFile(dir: UniFile?, context: Context?) {
|
||||||
|
if (dir != null && dir.exists()) {
|
||||||
|
val nomedia = dir.findFile(NOMEDIA_FILE)
|
||||||
|
if (nomedia == null) {
|
||||||
|
dir.createFile(NOMEDIA_FILE)
|
||||||
|
context?.let { scanMedia(it, dir.uri) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the given file so that it can be shown in gallery apps, for example.
|
||||||
|
*/
|
||||||
|
fun scanMedia(context: Context, uri: Uri) {
|
||||||
|
MediaScannerConnection.scanFile(context, arrayOf(uri.path), null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutate the given filename to make it valid for a FAT filesystem,
|
||||||
|
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
|
||||||
|
* with a dot), but you can manually add it later.
|
||||||
|
*/
|
||||||
|
fun buildValidFilename(origName: String): String {
|
||||||
|
val name = origName.trim('.', ' ')
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
return "(invalid)"
|
||||||
|
}
|
||||||
|
val sb = StringBuilder(name.length)
|
||||||
|
name.forEach { c ->
|
||||||
|
if (isValidFatFilenameChar(c)) {
|
||||||
|
sb.append(c)
|
||||||
|
} else {
|
||||||
|
sb.append('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
|
||||||
|
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
|
||||||
|
return sb.toString().take(240)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given character is a valid filename character, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isValidFatFilenameChar(c: Char): Boolean {
|
||||||
|
if (0x00.toChar() <= c && c <= 0x1f.toChar()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return when (c) {
|
||||||
|
'"', '*', '/', ':', '<', '>', '?', '\\', '|', 0x7f.toChar() -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val NOMEDIA_FILE = ".nomedia"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@file:Suppress("PackageDirectoryMismatch")
|
||||||
|
|
||||||
|
package androidx.preference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
|
||||||
|
*/
|
||||||
|
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
|
||||||
|
return onBindEditTextListener
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package eu.kanade.tachiyomi.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import eu.kanade.domain.base.BasePreferences
|
||||||
|
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom [TextInputEditText] that sets [EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING] to imeOptions
|
||||||
|
* if [BasePreferences.incognitoMode] is true. Some IMEs may not respect this flag.
|
||||||
|
*
|
||||||
|
* @see setIncognito
|
||||||
|
*/
|
||||||
|
class TachiyomiTextInputEditText @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
) : TextInputEditText(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
setIncognito(scope!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
scope?.cancel()
|
||||||
|
scope = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Sets Flow to this [EditText] that sets [EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING] to imeOptions
|
||||||
|
* if [BasePreferences.incognitoMode] is true. Some IMEs may not respect this flag.
|
||||||
|
*/
|
||||||
|
fun EditText.setIncognito(viewScope: CoroutineScope) {
|
||||||
|
Injekt.get<BasePreferences>().incognitoMode().changes()
|
||||||
|
.onEach {
|
||||||
|
imeOptions = if (it) {
|
||||||
|
imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||||
|
} else {
|
||||||
|
imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(viewScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package tachiyomi.core.metadata.tachiyomi
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class AnimeDetails(
|
||||||
|
val title: String? = null,
|
||||||
|
val author: String? = null,
|
||||||
|
val artist: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val genre: List<String>? = null,
|
||||||
|
val status: Int? = null,
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package tachiyomi.core.metadata.tachiyomi
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaDetails(
|
||||||
|
val title: String? = null,
|
||||||
|
val author: String? = null,
|
||||||
|
val artist: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val genre: List<String>? = null,
|
||||||
|
val status: Int? = null,
|
||||||
|
)
|
||||||
22
app/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt
Normal file
22
app/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package tachiyomi.domain.entries
|
||||||
|
|
||||||
|
enum class TriStateFilter {
|
||||||
|
DISABLED, // Disable filter
|
||||||
|
ENABLED_IS, // Enabled with "is" filter
|
||||||
|
ENABLED_NOT, // Enabled with "not" filter
|
||||||
|
;
|
||||||
|
|
||||||
|
fun next(): TriStateFilter {
|
||||||
|
return when (this) {
|
||||||
|
DISABLED -> ENABLED_IS
|
||||||
|
ENABLED_IS -> ENABLED_NOT
|
||||||
|
ENABLED_NOT -> DISABLED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun applyFilter(filter: TriStateFilter, predicate: () -> Boolean): Boolean = when (filter) {
|
||||||
|
TriStateFilter.DISABLED -> true
|
||||||
|
TriStateFilter.ENABLED_IS -> predicate()
|
||||||
|
TriStateFilter.ENABLED_NOT -> !predicate()
|
||||||
|
}
|
||||||
134
app/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt
Normal file
134
app/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package tachiyomi.domain.entries.anime.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import tachiyomi.domain.entries.TriStateFilter
|
||||||
|
import java.io.Serializable
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
data class Anime(
|
||||||
|
val id: Long,
|
||||||
|
val source: Long,
|
||||||
|
val favorite: Boolean,
|
||||||
|
val lastUpdate: Long,
|
||||||
|
val nextUpdate: Long,
|
||||||
|
val calculateInterval: Int,
|
||||||
|
val dateAdded: Long,
|
||||||
|
val viewerFlags: Long,
|
||||||
|
val episodeFlags: Long,
|
||||||
|
val coverLastModified: Long,
|
||||||
|
val url: String,
|
||||||
|
val title: String,
|
||||||
|
val artist: String?,
|
||||||
|
val author: String?,
|
||||||
|
val description: String?,
|
||||||
|
val genre: List<String>?,
|
||||||
|
val status: Long,
|
||||||
|
val thumbnailUrl: String?,
|
||||||
|
val updateStrategy: UpdateStrategy,
|
||||||
|
val initialized: Boolean,
|
||||||
|
) : Serializable {
|
||||||
|
|
||||||
|
val sorting: Long
|
||||||
|
get() = episodeFlags and EPISODE_SORTING_MASK
|
||||||
|
|
||||||
|
val displayMode: Long
|
||||||
|
get() = episodeFlags and EPISODE_DISPLAY_MASK
|
||||||
|
|
||||||
|
val unseenFilterRaw: Long
|
||||||
|
get() = episodeFlags and EPISODE_UNSEEN_MASK
|
||||||
|
|
||||||
|
val downloadedFilterRaw: Long
|
||||||
|
get() = episodeFlags and EPISODE_DOWNLOADED_MASK
|
||||||
|
|
||||||
|
val bookmarkedFilterRaw: Long
|
||||||
|
get() = episodeFlags and EPISODE_BOOKMARKED_MASK
|
||||||
|
|
||||||
|
val skipIntroLength: Int
|
||||||
|
get() = (viewerFlags and ANIME_INTRO_MASK).toInt()
|
||||||
|
|
||||||
|
val nextEpisodeToAir: Int
|
||||||
|
get() = (viewerFlags and ANIME_AIRING_EPISODE_MASK).removeHexZeros(zeros = 2).toInt()
|
||||||
|
|
||||||
|
val nextEpisodeAiringAt: Long
|
||||||
|
get() = (viewerFlags and ANIME_AIRING_TIME_MASK).removeHexZeros(zeros = 6)
|
||||||
|
|
||||||
|
val unseenFilter: TriStateFilter
|
||||||
|
get() = when (unseenFilterRaw) {
|
||||||
|
EPISODE_SHOW_UNSEEN -> TriStateFilter.ENABLED_IS
|
||||||
|
EPISODE_SHOW_SEEN -> TriStateFilter.ENABLED_NOT
|
||||||
|
else -> TriStateFilter.DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
val bookmarkedFilter: TriStateFilter
|
||||||
|
get() = when (bookmarkedFilterRaw) {
|
||||||
|
EPISODE_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
|
||||||
|
EPISODE_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
|
||||||
|
else -> TriStateFilter.DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sortDescending(): Boolean {
|
||||||
|
return episodeFlags and EPISODE_SORT_DIR_MASK == EPISODE_SORT_DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Long.removeHexZeros(zeros: Int): Long {
|
||||||
|
val hex = 16.0
|
||||||
|
return this.div(hex.pow(zeros)).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Generic filter that does not filter anything
|
||||||
|
const val SHOW_ALL = 0x00000000L
|
||||||
|
|
||||||
|
const val EPISODE_SORT_DESC = 0x00000000L
|
||||||
|
const val EPISODE_SORT_ASC = 0x00000001L
|
||||||
|
const val EPISODE_SORT_DIR_MASK = 0x00000001L
|
||||||
|
|
||||||
|
const val EPISODE_SHOW_UNSEEN = 0x00000002L
|
||||||
|
const val EPISODE_SHOW_SEEN = 0x00000004L
|
||||||
|
const val EPISODE_UNSEEN_MASK = 0x00000006L
|
||||||
|
|
||||||
|
const val EPISODE_SHOW_DOWNLOADED = 0x00000008L
|
||||||
|
const val EPISODE_SHOW_NOT_DOWNLOADED = 0x00000010L
|
||||||
|
const val EPISODE_DOWNLOADED_MASK = 0x00000018L
|
||||||
|
|
||||||
|
const val EPISODE_SHOW_BOOKMARKED = 0x00000020L
|
||||||
|
const val EPISODE_SHOW_NOT_BOOKMARKED = 0x00000040L
|
||||||
|
const val EPISODE_BOOKMARKED_MASK = 0x00000060L
|
||||||
|
|
||||||
|
const val EPISODE_SORTING_SOURCE = 0x00000000L
|
||||||
|
const val EPISODE_SORTING_NUMBER = 0x00000100L
|
||||||
|
const val EPISODE_SORTING_UPLOAD_DATE = 0x00000200L
|
||||||
|
const val EPISODE_SORTING_MASK = 0x00000300L
|
||||||
|
|
||||||
|
const val EPISODE_DISPLAY_NAME = 0x00000000L
|
||||||
|
const val EPISODE_DISPLAY_NUMBER = 0x00100000L
|
||||||
|
const val EPISODE_DISPLAY_MASK = 0x00100000L
|
||||||
|
|
||||||
|
const val ANIME_INTRO_MASK = 0x000000000000FFL
|
||||||
|
const val ANIME_AIRING_EPISODE_MASK = 0x00000000FFFF00L
|
||||||
|
const val ANIME_AIRING_TIME_MASK = 0xFFFFFFFF000000L
|
||||||
|
|
||||||
|
fun create() = Anime(
|
||||||
|
id = -1L,
|
||||||
|
url = "",
|
||||||
|
title = "",
|
||||||
|
source = -1L,
|
||||||
|
favorite = false,
|
||||||
|
lastUpdate = 0L,
|
||||||
|
nextUpdate = 0L,
|
||||||
|
calculateInterval = 0,
|
||||||
|
dateAdded = 0L,
|
||||||
|
viewerFlags = 0L,
|
||||||
|
episodeFlags = 0L,
|
||||||
|
coverLastModified = 0L,
|
||||||
|
artist = null,
|
||||||
|
author = null,
|
||||||
|
description = null,
|
||||||
|
genre = null,
|
||||||
|
status = 0L,
|
||||||
|
thumbnailUrl = null,
|
||||||
|
updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||||
|
initialized = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
115
app/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt
Normal file
115
app/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package tachiyomi.domain.entries.manga.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import tachiyomi.domain.entries.TriStateFilter
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class Manga(
|
||||||
|
val id: Long,
|
||||||
|
val source: Long,
|
||||||
|
val favorite: Boolean,
|
||||||
|
val lastUpdate: Long,
|
||||||
|
val nextUpdate: Long,
|
||||||
|
val calculateInterval: Int,
|
||||||
|
val dateAdded: Long,
|
||||||
|
val viewerFlags: Long,
|
||||||
|
val chapterFlags: Long,
|
||||||
|
val coverLastModified: Long,
|
||||||
|
val url: String,
|
||||||
|
val title: String,
|
||||||
|
val artist: String?,
|
||||||
|
val author: String?,
|
||||||
|
val description: String?,
|
||||||
|
val genre: List<String>?,
|
||||||
|
val status: Long,
|
||||||
|
val thumbnailUrl: String?,
|
||||||
|
val updateStrategy: UpdateStrategy,
|
||||||
|
val initialized: Boolean,
|
||||||
|
) : Serializable {
|
||||||
|
|
||||||
|
val sorting: Long
|
||||||
|
get() = chapterFlags and CHAPTER_SORTING_MASK
|
||||||
|
|
||||||
|
val displayMode: Long
|
||||||
|
get() = chapterFlags and CHAPTER_DISPLAY_MASK
|
||||||
|
|
||||||
|
val unreadFilterRaw: Long
|
||||||
|
get() = chapterFlags and CHAPTER_UNREAD_MASK
|
||||||
|
|
||||||
|
val downloadedFilterRaw: Long
|
||||||
|
get() = chapterFlags and CHAPTER_DOWNLOADED_MASK
|
||||||
|
|
||||||
|
val bookmarkedFilterRaw: Long
|
||||||
|
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
|
||||||
|
|
||||||
|
val unreadFilter: TriStateFilter
|
||||||
|
get() = when (unreadFilterRaw) {
|
||||||
|
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
|
||||||
|
CHAPTER_SHOW_READ -> TriStateFilter.ENABLED_NOT
|
||||||
|
else -> TriStateFilter.DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
val bookmarkedFilter: TriStateFilter
|
||||||
|
get() = when (bookmarkedFilterRaw) {
|
||||||
|
CHAPTER_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
|
||||||
|
CHAPTER_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
|
||||||
|
else -> TriStateFilter.DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sortDescending(): Boolean {
|
||||||
|
return chapterFlags and CHAPTER_SORT_DIR_MASK == CHAPTER_SORT_DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Generic filter that does not filter anything
|
||||||
|
const val SHOW_ALL = 0x00000000L
|
||||||
|
|
||||||
|
const val CHAPTER_SORT_DESC = 0x00000000L
|
||||||
|
const val CHAPTER_SORT_ASC = 0x00000001L
|
||||||
|
const val CHAPTER_SORT_DIR_MASK = 0x00000001L
|
||||||
|
|
||||||
|
const val CHAPTER_SHOW_UNREAD = 0x00000002L
|
||||||
|
const val CHAPTER_SHOW_READ = 0x00000004L
|
||||||
|
const val CHAPTER_UNREAD_MASK = 0x00000006L
|
||||||
|
|
||||||
|
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008L
|
||||||
|
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010L
|
||||||
|
const val CHAPTER_DOWNLOADED_MASK = 0x00000018L
|
||||||
|
|
||||||
|
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020L
|
||||||
|
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040L
|
||||||
|
const val CHAPTER_BOOKMARKED_MASK = 0x00000060L
|
||||||
|
|
||||||
|
const val CHAPTER_SORTING_SOURCE = 0x00000000L
|
||||||
|
const val CHAPTER_SORTING_NUMBER = 0x00000100L
|
||||||
|
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200L
|
||||||
|
const val CHAPTER_SORTING_MASK = 0x00000300L
|
||||||
|
|
||||||
|
const val CHAPTER_DISPLAY_NAME = 0x00000000L
|
||||||
|
const val CHAPTER_DISPLAY_NUMBER = 0x00100000L
|
||||||
|
const val CHAPTER_DISPLAY_MASK = 0x00100000L
|
||||||
|
|
||||||
|
fun create() = Manga(
|
||||||
|
id = -1L,
|
||||||
|
url = "",
|
||||||
|
title = "",
|
||||||
|
source = -1L,
|
||||||
|
favorite = false,
|
||||||
|
lastUpdate = 0L,
|
||||||
|
nextUpdate = 0L,
|
||||||
|
calculateInterval = 0,
|
||||||
|
dateAdded = 0L,
|
||||||
|
viewerFlags = 0L,
|
||||||
|
chapterFlags = 0L,
|
||||||
|
coverLastModified = 0L,
|
||||||
|
artist = null,
|
||||||
|
author = null,
|
||||||
|
description = null,
|
||||||
|
genre = null,
|
||||||
|
status = 0L,
|
||||||
|
thumbnailUrl = null,
|
||||||
|
updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||||
|
initialized = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package tachiyomi.domain.items.episode.service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -R> = regex conversion.
|
||||||
|
*/
|
||||||
|
object EpisodeRecognition {
|
||||||
|
|
||||||
|
private const val NUMBER_PATTERN = """([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All cases with Ch.xx
|
||||||
|
* Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation -R> 4
|
||||||
|
*/
|
||||||
|
private val basic = Regex("""(?<=ep\.) *$NUMBER_PATTERN""")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Bleach 567: Down With Snowwhite -R> 567
|
||||||
|
*/
|
||||||
|
private val number = Regex(NUMBER_PATTERN)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex used to remove unwanted tags
|
||||||
|
* Example Prison School 12 v.1 vol004 version1243 volume64 -R> Prison School 12
|
||||||
|
*/
|
||||||
|
private val unwanted = Regex("""\b(?:v|ver|vol|version|volume|season|s)[^a-z]?[0-9]+""")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex used to remove unwanted whitespace
|
||||||
|
* Example One Piece 12 special -R> One Piece 12special
|
||||||
|
*/
|
||||||
|
private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""")
|
||||||
|
|
||||||
|
fun parseEpisodeNumber(animeTitle: String, episodeName: String, episodeNumber: Float? = null): Float {
|
||||||
|
// If episode number is known return.
|
||||||
|
if (episodeNumber != null && (episodeNumber == -2f || episodeNumber > -1f)) {
|
||||||
|
return episodeNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get chapter title with lower case
|
||||||
|
var name = episodeName.lowercase()
|
||||||
|
|
||||||
|
// Remove anime title from episode title.
|
||||||
|
name = name.replace(animeTitle.lowercase(), "").trim()
|
||||||
|
|
||||||
|
// Remove comma's or hyphens.
|
||||||
|
name = name.replace(',', '.').replace('-', '.')
|
||||||
|
|
||||||
|
// Remove unwanted white spaces.
|
||||||
|
name = unwantedWhiteSpace.replace(name, "")
|
||||||
|
|
||||||
|
// Remove unwanted tags.
|
||||||
|
name = unwanted.replace(name, "")
|
||||||
|
|
||||||
|
// Check base case ch.xx
|
||||||
|
basic.find(name)?.let { return getEpisodeNumberFromMatch(it) }
|
||||||
|
|
||||||
|
// Take the first number encountered.
|
||||||
|
number.find(name)?.let { return getEpisodeNumberFromMatch(it) }
|
||||||
|
|
||||||
|
return episodeNumber ?: -1f
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if episode number is found and return it
|
||||||
|
* @param match result of regex
|
||||||
|
* @return chapter number if found else null
|
||||||
|
*/
|
||||||
|
private fun getEpisodeNumberFromMatch(match: MatchResult): Float {
|
||||||
|
return match.let {
|
||||||
|
val initial = it.groups[1]?.value?.toFloat()!!
|
||||||
|
val subChapterDecimal = it.groups[2]?.value
|
||||||
|
val subChapterAlpha = it.groups[3]?.value
|
||||||
|
val addition = checkForDecimal(subChapterDecimal, subChapterAlpha)
|
||||||
|
initial.plus(addition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for decimal in received strings
|
||||||
|
* @param decimal decimal value of regex
|
||||||
|
* @param alpha alpha value of regex
|
||||||
|
* @return decimal/alpha float value
|
||||||
|
*/
|
||||||
|
private fun checkForDecimal(decimal: String?, alpha: String?): Float {
|
||||||
|
if (!decimal.isNullOrEmpty()) {
|
||||||
|
return decimal.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alpha.isNullOrEmpty()) {
|
||||||
|
if (alpha.contains("extra")) {
|
||||||
|
return .99f
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alpha.contains("omake")) {
|
||||||
|
return .98f
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alpha.contains("special")) {
|
||||||
|
return .97f
|
||||||
|
}
|
||||||
|
|
||||||
|
val trimmedAlpha = alpha.trimStart('.')
|
||||||
|
if (trimmedAlpha.length == 1) {
|
||||||
|
return parseAlphaPostFix(trimmedAlpha[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .0f
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* x.a -> x.1, x.b -> x.2, etc
|
||||||
|
*/
|
||||||
|
private fun parseAlphaPostFix(alpha: Char): Float {
|
||||||
|
val number = alpha.code - ('a'.code - 1)
|
||||||
|
if (number >= 10) return 0f
|
||||||
|
return number / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package tachiyomi.domain.source.anime.model
|
||||||
|
|
||||||
|
data class AnimeSource(
|
||||||
|
val id: Long,
|
||||||
|
val lang: String,
|
||||||
|
val name: String,
|
||||||
|
val supportsLatest: Boolean,
|
||||||
|
val isStub: Boolean,
|
||||||
|
val pin: Pins = Pins.unpinned,
|
||||||
|
val isUsedLast: Boolean = false,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val visualName: String
|
||||||
|
get() = when {
|
||||||
|
lang.isEmpty() -> name
|
||||||
|
else -> "$name (${lang.uppercase()})"
|
||||||
|
}
|
||||||
|
|
||||||
|
val key: () -> String = {
|
||||||
|
when {
|
||||||
|
isUsedLast -> "$id-lastused"
|
||||||
|
else -> "$id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package tachiyomi.domain.source.anime.model
|
||||||
|
|
||||||
|
data class AnimeSourceWithCount(
|
||||||
|
val source: AnimeSource,
|
||||||
|
val count: Long,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val id: Long
|
||||||
|
get() = source.id
|
||||||
|
|
||||||
|
val name: String
|
||||||
|
get() = source.name
|
||||||
|
}
|
||||||
42
app/src/main/java/tachiyomi/domain/source/anime/model/Pin.kt
Normal file
42
app/src/main/java/tachiyomi/domain/source/anime/model/Pin.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package tachiyomi.domain.source.anime.model
|
||||||
|
|
||||||
|
sealed class Pin(val code: Int) {
|
||||||
|
object Unpinned : Pin(0b00)
|
||||||
|
object Pinned : Pin(0b01)
|
||||||
|
object Actual : Pin(0b10)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
|
||||||
|
return Pins.PinsBuilder().apply(builder).flags()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Pins(vararg pins: Pin) = Pins {
|
||||||
|
pins.forEach { +it }
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Pins(val code: Int = Pin.Unpinned.code) {
|
||||||
|
|
||||||
|
operator fun contains(pin: Pin): Boolean = pin.code and code == pin.code
|
||||||
|
|
||||||
|
operator fun plus(pin: Pin): Pins = Pins(code or pin.code)
|
||||||
|
|
||||||
|
operator fun minus(pin: Pin): Pins = Pins(code xor pin.code)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val unpinned = Pins(Pin.Unpinned)
|
||||||
|
|
||||||
|
val pinned = Pins(Pin.Pinned, Pin.Actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PinsBuilder(var code: Int = 0) {
|
||||||
|
operator fun Pin.unaryPlus() {
|
||||||
|
this@PinsBuilder.code = code or this@PinsBuilder.code
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun Pin.unaryMinus() {
|
||||||
|
this@PinsBuilder.code = code or this@PinsBuilder.code
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flags(): Pins = Pins(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package tachiyomi.domain.source.anime.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
|
||||||
|
@Suppress("OverridingDeprecatedMember")
|
||||||
|
class StubAnimeSource(private val sourceData: AnimeSourceData) : AnimeSource {
|
||||||
|
|
||||||
|
override val id: Long = sourceData.id
|
||||||
|
|
||||||
|
override val name: String = sourceData.name.ifBlank { id.toString() }
|
||||||
|
|
||||||
|
override val lang: String = sourceData.lang
|
||||||
|
|
||||||
|
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
||||||
|
throw AnimeSourceNotInstalledException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||||
|
throw AnimeSourceNotInstalledException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||||
|
throw AnimeSourceNotInstalledException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return if (sourceData.isMissingInfo.not()) "$name (${lang.uppercase()})" else id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class AnimeSourceNotInstalledException : Exception()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package tachiyomi.domain.source.anime.repository
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||||
|
import tachiyomi.domain.source.anime.model.AnimeSourceWithCount
|
||||||
|
|
||||||
|
typealias AnimeSourcePagingSourceType = PagingSource<Long, SAnime>
|
||||||
|
|
||||||
|
interface AnimeSourceRepository {
|
||||||
|
|
||||||
|
fun getAnimeSources(): Flow<List<AnimeSource>>
|
||||||
|
|
||||||
|
fun getOnlineAnimeSources(): Flow<List<AnimeSource>>
|
||||||
|
|
||||||
|
fun getAnimeSourcesWithFavoriteCount(): Flow<List<Pair<AnimeSource, Long>>>
|
||||||
|
|
||||||
|
fun getSourcesWithNonLibraryAnime(): Flow<List<AnimeSourceWithCount>>
|
||||||
|
|
||||||
|
fun searchAnime(sourceId: Long, query: String, filterList: AnimeFilterList): AnimeSourcePagingSourceType
|
||||||
|
|
||||||
|
fun getPopularAnime(sourceId: Long): AnimeSourcePagingSourceType
|
||||||
|
|
||||||
|
fun getLatestAnime(sourceId: Long): AnimeSourcePagingSourceType
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package tachiyomi.domain.source.anime.service
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import tachiyomi.domain.source.anime.model.StubAnimeSource
|
||||||
|
|
||||||
|
interface AnimeSourceManager {
|
||||||
|
|
||||||
|
val catalogueSources: Flow<List<AnimeCatalogueSource>>
|
||||||
|
|
||||||
|
fun get(sourceKey: Long): AnimeSource?
|
||||||
|
|
||||||
|
fun getOrStub(sourceKey: Long): AnimeSource
|
||||||
|
|
||||||
|
fun getOnlineSources(): List<AnimeHttpSource>
|
||||||
|
|
||||||
|
fun getCatalogueSources(): List<AnimeCatalogueSource>
|
||||||
|
|
||||||
|
fun getStubSources(): List<StubAnimeSource>
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package tachiyomi.domain.source.manga.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.MangaSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
@Suppress("OverridingDeprecatedMember")
|
||||||
|
class StubMangaSource(private val sourceData: MangaSourceData) : MangaSource {
|
||||||
|
|
||||||
|
override val id: Long = sourceData.id
|
||||||
|
|
||||||
|
override val name: String = sourceData.name.ifBlank { id.toString() }
|
||||||
|
|
||||||
|
override val lang: String = sourceData.lang
|
||||||
|
|
||||||
|
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||||
|
throw SourceNotInstalledException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
return Observable.error(SourceNotInstalledException())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||||
|
throw SourceNotInstalledException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return Observable.error(SourceNotInstalledException())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||||
|
throw SourceNotInstalledException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList"))
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
return Observable.error(SourceNotInstalledException())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return if (sourceData.isMissingInfo.not()) "$name (${lang.uppercase()})" else id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourceNotInstalledException : Exception()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package tachiyomi.domain.source.manga.service
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.MangaSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import tachiyomi.domain.source.manga.model.StubMangaSource
|
||||||
|
|
||||||
|
interface MangaSourceManager {
|
||||||
|
|
||||||
|
val catalogueSources: Flow<List<CatalogueSource>>
|
||||||
|
|
||||||
|
fun get(sourceKey: Long): MangaSource?
|
||||||
|
|
||||||
|
fun getOrStub(sourceKey: Long): MangaSource
|
||||||
|
|
||||||
|
fun getOnlineSources(): List<HttpSource>
|
||||||
|
|
||||||
|
fun getCatalogueSources(): List<CatalogueSource>
|
||||||
|
|
||||||
|
fun getStubSources(): List<StubMangaSource>
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package tachiyomi.source.local.entries.anime
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.UnmeteredSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
//import eu.kanade.tachiyomi.util.storage.toFFmpegString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import rx.Observable
|
||||||
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
|
import tachiyomi.source.local.filter.anime.AnimeOrderBy
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class LocalAnimeSource(
|
||||||
|
private val context: Context,
|
||||||
|
) : AnimeCatalogueSource, UnmeteredSource {
|
||||||
|
|
||||||
|
private val POPULAR_FILTERS = AnimeFilterList(AnimeOrderBy.Popular(context))
|
||||||
|
private val LATEST_FILTERS = AnimeFilterList(AnimeOrderBy.Latest(context))
|
||||||
|
|
||||||
|
override val name ="Local anime source"
|
||||||
|
|
||||||
|
override val id: Long = ID
|
||||||
|
|
||||||
|
override val lang = "other"
|
||||||
|
|
||||||
|
override fun toString() = name
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
// Browse related
|
||||||
|
override fun fetchPopularAnime(page: Int) = fetchSearchAnime(page, "", POPULAR_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int) = fetchSearchAnime(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||||
|
//return emptyObservable()
|
||||||
|
return Observable.just(AnimesPage(emptyList(), false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anime details related
|
||||||
|
override suspend fun getAnimeDetails(anime: SAnime): SAnime = withIOContext {
|
||||||
|
//return empty
|
||||||
|
anime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Episodes
|
||||||
|
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||||
|
//return empty
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
override fun getFilterList() = AnimeFilterList(AnimeOrderBy.Popular(context))
|
||||||
|
|
||||||
|
// Unused stuff
|
||||||
|
override suspend fun getVideoList(episode: SEpisode) = throw UnsupportedOperationException("Unused")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ID = 0L
|
||||||
|
const val HELP_URL = "https://aniyomi.org/help/guides/local-anime/"
|
||||||
|
|
||||||
|
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||||
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
|
|
||||||
|
private fun getBaseDirectories(context: Context): Sequence<File> {
|
||||||
|
val localFolder = "Aniyomi" + File.separator + "localanime"
|
||||||
|
return DiskUtil.getExternalStorages(context)
|
||||||
|
.map { File(it.absolutePath, localFolder) }
|
||||||
|
.asSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
|
||||||
|
return getBaseDirectories(context)
|
||||||
|
// Get all the files inside all baseDir
|
||||||
|
.flatMap { it.listFiles().orEmpty().toList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAnimeDir(animeUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||||
|
return baseDirsFile
|
||||||
|
// Get the first animeDir or null
|
||||||
|
.firstOrNull { it.isDirectory && it.name == animeUrl }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Anime.isLocal(): Boolean = source == LocalAnimeSource.ID
|
||||||
|
|
||||||
|
fun AnimeSource.isLocal(): Boolean = id == LocalAnimeSource.ID
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package tachiyomi.source.local.entries.manga
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.MangaSource
|
||||||
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import rx.Observable
|
||||||
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
|
import tachiyomi.source.local.filter.manga.MangaOrderBy
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class LocalMangaSource(
|
||||||
|
private val context: Context,
|
||||||
|
) : CatalogueSource, UnmeteredSource {
|
||||||
|
|
||||||
|
|
||||||
|
private val POPULAR_FILTERS = FilterList(MangaOrderBy.Popular(context))
|
||||||
|
private val LATEST_FILTERS = FilterList(MangaOrderBy.Latest(context))
|
||||||
|
|
||||||
|
override val name: String = "Local manga source"
|
||||||
|
|
||||||
|
override val id: Long = ID
|
||||||
|
|
||||||
|
override val lang: String = "other"
|
||||||
|
|
||||||
|
override fun toString() = name
|
||||||
|
|
||||||
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
|
// Browse related
|
||||||
|
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return Observable.just(MangasPage(emptyList(), false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manga details related
|
||||||
|
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
|
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
override fun getFilterList() = FilterList(MangaOrderBy.Popular(context))
|
||||||
|
|
||||||
|
// Unused stuff
|
||||||
|
override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ID = 0L
|
||||||
|
const val HELP_URL = "https://aniyomi.org/help/guides/local-manga/"
|
||||||
|
|
||||||
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Manga.isLocal(): Boolean = source == LocalMangaSource.ID
|
||||||
|
|
||||||
|
fun MangaSource.isLocal(): Boolean = id == LocalMangaSource.ID
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user