extension settings
|
@ -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'
|
||||||
|
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 16 KiB |
|
@ -85,14 +85,20 @@ class MainActivity : AppCompatActivity() {
|
||||||
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
|
||||||
val backgroundDrawable = bottomBar.background as GradientDrawable
|
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||||
backgroundDrawable.setColor(semiTransparentColor)
|
backgroundDrawable.setColor(semiTransparentColor)
|
||||||
bottomBar.background = backgroundDrawable
|
_bottomBar.background = backgroundDrawable
|
||||||
|
}
|
||||||
|
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("colorOverflow", false)
|
||||||
|
if (!colorOverflow) {
|
||||||
|
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -19,6 +22,7 @@ import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.MediaPageTransformer
|
import ani.dantotsu.MediaPageTransformer
|
||||||
import ani.dantotsu.R
|
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
|
||||||
|
@ -58,6 +62,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -19,6 +21,7 @@ import ani.dantotsu.media.GenreActivity
|
||||||
import ani.dantotsu.MediaPageTransformer
|
import ani.dantotsu.MediaPageTransformer
|
||||||
import ani.dantotsu.R
|
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
|
||||||
|
@ -57,6 +60,16 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||||
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
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.util.TypedValue
|
||||||
|
@ -8,23 +10,38 @@ 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.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,
|
||||||
|
@ -70,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 {
|
||||||
|
@ -92,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,
|
||||||
|
@ -263,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,6 +289,74 @@ class AnimeWatchFragment : Fragment() {
|
||||||
else getString(R.string.unsubscribed_notification)
|
else getString(R.string.unsubscribed_notification)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fun openSettings(pkg: AnimeExtension.Installed){
|
||||||
|
val changeUIVisibility: (Boolean) -> Unit = { show ->
|
||||||
|
val activity = requireActivity() as MediaDetailsActivity
|
||||||
|
val visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
|
||||||
|
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||||
|
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||||
|
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||||
|
try{
|
||||||
|
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||||
|
}catch (e: ClassCastException){
|
||||||
|
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||||
|
}
|
||||||
|
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||||
|
if (show) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
|
||||||
|
if (allSettings.isNotEmpty()) {
|
||||||
|
var selectedSetting = allSettings[0]
|
||||||
|
if (allSettings.size > 1) {
|
||||||
|
val names = allSettings.map { it.lang }.toTypedArray()
|
||||||
|
var selectedIndex = 0
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle("Select a Source")
|
||||||
|
.setSingleChoiceItems(names, selectedIndex) { _, which ->
|
||||||
|
selectedIndex = which
|
||||||
|
}
|
||||||
|
.setPositiveButton("OK") { dialog, _ ->
|
||||||
|
selectedSetting = allSettings[selectedIndex]
|
||||||
|
dialog.dismiss()
|
||||||
|
|
||||||
|
// Move the fragment transaction here
|
||||||
|
val fragment =
|
||||||
|
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||||
|
changeUIVisibility(true)
|
||||||
|
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||||
|
}
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||||
|
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
dialog.cancel()
|
||||||
|
changeUIVisibility(true)
|
||||||
|
return@setNegativeButton
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
// If there's only one setting, proceed with the fragment transaction
|
||||||
|
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
|
||||||
|
changeUIVisibility(true)
|
||||||
|
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||||
|
}
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||||
|
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
changeUIVisibility(false)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onEpisodeClick(i: String) {
|
fun onEpisodeClick(i: String) {
|
||||||
model.continueMedia = false
|
model.continueMedia = false
|
||||||
|
@ -274,8 +369,10 @@ class AnimeWatchFragment : Fragment() {
|
||||||
val selected = model.loadSelected(media)
|
val selected = model.loadSelected(media)
|
||||||
|
|
||||||
//Find latest episode for subscription
|
//Find latest episode for subscription
|
||||||
selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
selected.latest =
|
||||||
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: 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())
|
model.saveSelected(media.id, selected, requireActivity())
|
||||||
headerAdapter.handleEpisodes()
|
headerAdapter.handleEpisodes()
|
||||||
|
|
|
@ -17,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,
|
||||||
|
@ -50,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 } }
|
||||||
|
@ -65,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
|
||||||
|
@ -224,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 {
|
||||||
|
|
|
@ -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
|
||||||
|
@ -62,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
|
||||||
}
|
}
|
||||||
|
@ -71,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)
|
||||||
|
@ -91,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)
|
||||||
|
@ -108,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)
|
||||||
|
@ -174,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
|
||||||
}
|
}
|
||||||
|
@ -183,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)
|
||||||
|
@ -201,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 {
|
||||||
|
@ -321,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()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ani.dantotsu.settings
|
package ani.dantotsu.settings
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -7,8 +8,10 @@ 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.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -17,10 +20,15 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
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.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 com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
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
|
||||||
|
@ -30,12 +38,78 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class InstalledAnimeExtensionsFragment : Fragment() {
|
class InstalledAnimeExtensionsFragment : Fragment() {
|
||||||
|
|
||||||
|
|
||||||
private var _binding: FragmentAnimeExtensionsBinding? = null
|
private var _binding: FragmentAnimeExtensionsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private lateinit var extensionsRecyclerView: RecyclerView
|
private lateinit var extensionsRecyclerView: RecyclerView
|
||||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||||
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
|
private val animeExtensionManager: AnimeExtensionManager = Injekt.get()
|
||||||
private val extensionsAdapter = AnimeExtensionsAdapter({ pkg ->
|
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
|
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||||
val context = requireContext() // Store context in a variable
|
val context = requireContext() // Store context in a variable
|
||||||
val notificationManager =
|
val notificationManager =
|
||||||
|
@ -85,7 +159,8 @@ class InstalledAnimeExtensionsFragment : Fragment() {
|
||||||
animeExtensionManager.uninstallExtension(pkg.pkgName)
|
animeExtensionManager.uninstallExtension(pkg.pkgName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, skipIcons)
|
}, skipIcons
|
||||||
|
)
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -114,6 +189,7 @@ class InstalledAnimeExtensionsFragment : Fragment() {
|
||||||
|
|
||||||
|
|
||||||
private class AnimeExtensionsAdapter(
|
private class AnimeExtensionsAdapter(
|
||||||
|
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
|
||||||
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
|
private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
|
||||||
skipIcons: Boolean
|
skipIcons: Boolean
|
||||||
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
|
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
|
||||||
|
@ -152,10 +228,14 @@ class InstalledAnimeExtensionsFragment : Fragment() {
|
||||||
holder.closeTextView.setOnClickListener {
|
holder.closeTextView.setOnClickListener {
|
||||||
onUninstallClicked(extension)
|
onUninstallClicked(extension)
|
||||||
}
|
}
|
||||||
|
holder.settingsImageView.setOnClickListener {
|
||||||
|
onSettingsClicked(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
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 extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||||
}
|
}
|
||||||
|
@ -180,5 +260,4 @@ class InstalledAnimeExtensionsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package ani.dantotsu.settings
|
package ani.dantotsu.settings
|
||||||
|
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -8,8 +9,10 @@ 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.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -18,13 +21,18 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
import ani.dantotsu.databinding.FragmentMangaExtensionsBinding
|
||||||
import ani.dantotsu.loadData
|
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 com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
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 eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -37,6 +45,66 @@ class InstalledMangaExtensionsFragment : Fragment() {
|
||||||
val skipIcons = loadData("skip_extension_icons") ?: false
|
val skipIcons = loadData("skip_extension_icons") ?: false
|
||||||
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
|
private val mangaExtensionManager: MangaExtensionManager = Injekt.get()
|
||||||
private val extensionsAdapter = MangaExtensionsAdapter({ pkg ->
|
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
|
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||||
val context = requireContext() // Store context in a variable
|
val context = requireContext() // Store context in a variable
|
||||||
val notificationManager =
|
val notificationManager =
|
||||||
|
@ -115,6 +183,7 @@ class InstalledMangaExtensionsFragment : Fragment() {
|
||||||
|
|
||||||
|
|
||||||
private class MangaExtensionsAdapter(
|
private class MangaExtensionsAdapter(
|
||||||
|
private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
|
||||||
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
|
private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
|
||||||
skipIcons: Boolean
|
skipIcons: Boolean
|
||||||
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
|
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
|
||||||
|
@ -153,10 +222,14 @@ class InstalledMangaExtensionsFragment : Fragment() {
|
||||||
holder.closeTextView.setOnClickListener {
|
holder.closeTextView.setOnClickListener {
|
||||||
onUninstallClicked(extension)
|
onUninstallClicked(extension)
|
||||||
}
|
}
|
||||||
|
holder.settingsImageView.setOnClickListener {
|
||||||
|
onSettingsClicked(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView)
|
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 extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
|
||||||
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
val closeTextView: TextView = view.findViewById(R.id.closeTextView)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,8 @@
|
||||||
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A source that explicitly doesn't require traffic considerations.
|
||||||
|
*
|
||||||
|
* This typically applies for self-hosted sources.
|
||||||
|
*/
|
||||||
|
interface UnmeteredSource
|
|
@ -0,0 +1,68 @@
|
||||||
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceDataStore
|
||||||
|
|
||||||
|
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
|
||||||
|
|
||||||
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
|
return prefs.getBoolean(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
|
prefs.edit {
|
||||||
|
putBoolean(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInt(key: String?, defValue: Int): Int {
|
||||||
|
return prefs.getInt(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String?, value: Int) {
|
||||||
|
prefs.edit {
|
||||||
|
putInt(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(key: String?, defValue: Long): Long {
|
||||||
|
return prefs.getLong(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putLong(key: String?, value: Long) {
|
||||||
|
prefs.edit {
|
||||||
|
putLong(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFloat(key: String?, defValue: Float): Float {
|
||||||
|
return prefs.getFloat(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putFloat(key: String?, value: Float) {
|
||||||
|
prefs.edit {
|
||||||
|
putFloat(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String?, defValue: String?): String? {
|
||||||
|
return prefs.getString(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putString(key: String?, value: String?) {
|
||||||
|
prefs.edit {
|
||||||
|
putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
|
||||||
|
return prefs.getStringSet(key, defValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
||||||
|
prefs.edit {
|
||||||
|
putStringSet(key, values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
|
@ -0,0 +1,14 @@
|
||||||
|
package tachiyomi.source.local.filter.anime
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
|
||||||
|
sealed class AnimeOrderBy(context: Context, selection: Selection) : AnimeFilter.Sort(
|
||||||
|
|
||||||
|
"Order by",
|
||||||
|
arrayOf("Title", "Date"),
|
||||||
|
selection,
|
||||||
|
) {
|
||||||
|
class Popular(context: Context) : AnimeOrderBy(context, Selection(0, true))
|
||||||
|
class Latest(context: Context) : AnimeOrderBy(context, Selection(1, false))
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package tachiyomi.source.local.filter.manga
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
sealed class MangaOrderBy(context: Context, selection: Selection) : Filter.Sort(
|
||||||
|
"Order by",
|
||||||
|
arrayOf("Title", "Date"),
|
||||||
|
selection,
|
||||||
|
) {
|
||||||
|
class Popular(context: Context) : MangaOrderBy(context, Selection(0, true))
|
||||||
|
class Latest(context: Context) : MangaOrderBy(context, Selection(1, false))
|
||||||
|
}
|
21
app/src/main/java/tachiyomi/source/local/io/Archive.kt
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package tachiyomi.source.local.io
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object ArchiveAnime {
|
||||||
|
|
||||||
|
private val SUPPORTED_ARCHIVE_TYPES = listOf("mp4", "mkv")
|
||||||
|
|
||||||
|
fun isSupported(file: File): Boolean = with(file) {
|
||||||
|
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ArchiveManga {
|
||||||
|
|
||||||
|
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||||
|
|
||||||
|
fun isSupported(file: File): Boolean = with(file) {
|
||||||
|
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||||
|
}
|
||||||
|
}
|
7
app/src/main/res/anim/slide_down.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:fromYDelta="0%"
|
||||||
|
android:toYDelta="100%"
|
||||||
|
android:duration="300"/>
|
||||||
|
</set>
|
7
app/src/main/res/anim/slide_up.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:fromYDelta="100%"
|
||||||
|
android:toYDelta="0%"
|
||||||
|
android:duration="300"/>
|
||||||
|
</set>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<solid android:color="?attr/colorPrimaryContainer"/>
|
<solid android:color="?attr/colorOnPrimaryContainer"/>
|
||||||
<corners android:radius="40dp"/>
|
<corners android:radius="40dp"/>
|
||||||
</shape>
|
</shape>
|
5
app/src/main/res/drawable/bottom_nav_gray.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/grey_nav"/>
|
||||||
|
<corners android:radius="40dp"/>
|
||||||
|
</shape>
|
|
@ -5,13 +5,13 @@
|
||||||
android:viewportHeight="768">
|
android:viewportHeight="768">
|
||||||
<group>
|
<group>
|
||||||
<clip-path
|
<clip-path
|
||||||
android:pathData="M128,384a256,255.96 0,1 0,512 0a256,255.96 0,1 0,-512 0z"/>
|
android:pathData="M125.71,125.71h516.58v516.58h-516.58z"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M128,128h512v511.96h-512z"
|
android:pathData="M123.53,128.02h512v511.96h-512z"
|
||||||
android:strokeWidth="0"
|
android:strokeWidth="0"
|
||||||
android:fillColor="#ff00f4"/>
|
android:fillColor="#ff00f4"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="m128,128v335.26c23.32,3.7 47.23,5.63 71.58,5.63 211.59,0 389.34,-144.9 439.43,-340.89H128Z"
|
android:pathData="m117.58,129.49v335.26c23.32,3.7 47.23,5.63 71.58,5.63 211.59,0 389.34,-144.9 439.43,-340.89H117.58Z"
|
||||||
android:strokeWidth="0"
|
android:strokeWidth="0"
|
||||||
android:fillColor="#7000b8"/>
|
android:fillColor="#7000b8"/>
|
||||||
<path
|
<path
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
android:viewportHeight="768">
|
android:viewportHeight="768">
|
||||||
<group>
|
<group>
|
||||||
<clip-path
|
<clip-path
|
||||||
android:pathData="M128,384a256,255.96 0,1 0,512 0a256,255.96 0,1 0,-512 0z"/>
|
android:pathData="M125.71,125.71h516.58v516.58h-516.58z"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="m44.26,128c0,46.25 37.49,83.74 83.74,83.74h256c95.13,0 172.26,77.12 172.26,172.26h0c0,95.13 -77.12,172.26 -172.26,172.26H128c-46.24,0 -83.72,37.47 -83.74,83.71h723.74V128H44.26Z"
|
android:pathData="m44.26,128c0,46.25 37.49,83.74 83.74,83.74h256c95.13,0 172.26,77.12 172.26,172.26h0c0,95.13 -77.12,172.26 -172.26,172.26H128c-46.24,0 -83.72,37.47 -83.74,83.71h723.74V128H44.26Z"
|
||||||
android:strokeWidth="0"
|
android:strokeWidth="0"
|
||||||
|
|
|
@ -10,16 +10,16 @@
|
||||||
android:id="@+id/mediaTab"
|
android:id="@+id/mediaTab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorPrimaryContainer"
|
android:background="?attr/colorSurface"
|
||||||
android:translationZ="0dp"
|
android:translationZ="0dp"
|
||||||
app:itemPaddingTop="32dp"
|
app:itemPaddingTop="32dp"
|
||||||
app:menuGravity="center"
|
app:menuGravity="center"
|
||||||
app:itemActiveIndicatorStyle="@style/BottomNavBar"
|
app:itemActiveIndicatorStyle="@style/BottomNavBar"
|
||||||
app:itemIconTint="@color/tab_layout_icon"
|
app:itemIconTint="@color/tab_layout_icon"
|
||||||
app:itemRippleColor="?attr/colorSecondary"
|
app:itemRippleColor="?attr/colorPrimary"
|
||||||
app:itemTextAppearanceActive="@style/NavBarText"
|
app:itemTextAppearanceActive="@style/NavBarText"
|
||||||
app:itemTextAppearanceInactive="@style/NavBarText"
|
app:itemTextAppearanceInactive="@style/NavBarText"
|
||||||
app:itemTextColor="@color/tab_layout_text" />
|
app:itemTextColor="@color/tab_layout_icon" />
|
||||||
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -139,6 +139,7 @@
|
||||||
android:text="@string/add"
|
android:text="@string/add"
|
||||||
android:textAllCaps="true"
|
android:textAllCaps="true"
|
||||||
android:textColor="?attr/colorPrimary"
|
android:textColor="?attr/colorPrimary"
|
||||||
|
app:strokeColor="?attr/colorPrimary"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:cornerRadius="16dp"
|
app:cornerRadius="16dp"
|
||||||
|
@ -172,7 +173,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:background="?attr/colorOnBackground"
|
app:contentScrim="?android:colorBackground"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
|
@ -293,6 +294,13 @@
|
||||||
android:src="@drawable/ic_round_close_24"
|
android:src="@drawable/ic_round_close_24"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fragmentExtensionsContainer"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone">
|
||||||
|
</FrameLayout>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -133,6 +133,12 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"/>
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fragmentExtensionsContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone">
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -41,13 +41,13 @@
|
||||||
app:abb_animationDuration="300"
|
app:abb_animationDuration="300"
|
||||||
app:abb_animationInterpolator="@anim/over_shoot"
|
app:abb_animationInterpolator="@anim/over_shoot"
|
||||||
app:abb_badgeBackgroundColor="#F44336"
|
app:abb_badgeBackgroundColor="#F44336"
|
||||||
app:abb_indicatorColor="?attr/colorOnPrimaryContainer"
|
app:abb_indicatorColor="?attr/colorTertiary"
|
||||||
app:abb_indicatorLocation="bottom"
|
app:abb_indicatorLocation="bottom"
|
||||||
app:abb_indicatorMargin="28dp"
|
app:abb_indicatorMargin="28dp"
|
||||||
app:abb_selectedTabType="text"
|
app:abb_selectedTabType="text"
|
||||||
app:abb_tabColor="?attr/colorOnPrimary"
|
app:abb_tabColor="?attr/colorTertiary"
|
||||||
app:abb_tabColorDisabled="?attr/colorOnSecondary"
|
app:abb_tabColorDisabled="?attr/colorPrimaryContainer"
|
||||||
app:abb_tabColorSelected="?attr/colorOnPrimaryContainer"
|
app:abb_tabColorSelected="?attr/colorPrimary"
|
||||||
app:abb_tabs="@menu/bottom_navbar_menu"
|
app:abb_tabs="@menu/bottom_navbar_menu"
|
||||||
app:abb_textAppearance="@style/NavBarText"
|
app:abb_textAppearance="@style/NavBarText"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
|
@ -361,7 +361,8 @@
|
||||||
app:labelStyle="@style/fontTooltip"
|
app:labelStyle="@style/fontTooltip"
|
||||||
app:thumbRadius="8dp"
|
app:thumbRadius="8dp"
|
||||||
app:tickColor="#0000"
|
app:tickColor="#0000"
|
||||||
app:trackColorInactive="?android:colorBackground"
|
app:trackColorInactive="@color/grey_60"
|
||||||
|
app:trackColorActive="?attr/colorOnBackground"
|
||||||
app:trackHeight="2dp" />
|
app:trackHeight="2dp" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
|
@ -293,4 +293,12 @@
|
||||||
tools:ignore="ContentDescription,ImageContrastCheck"
|
tools:ignore="ContentDescription,ImageContrastCheck"
|
||||||
tools:srcCompat="@tools:sample/backgrounds/scenic[2]" />
|
tools:srcCompat="@tools:sample/backgrounds/scenic[2]" />
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fragmentExtensionsContainer"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone">
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -39,4 +39,6 @@
|
||||||
android:paddingBottom="128dp"
|
android:paddingBottom="128dp"
|
||||||
tools:itemCount="1"
|
tools:itemCount="1"
|
||||||
tools:listitem="@layout/item_anime_watch" />
|
tools:listitem="@layout/item_anime_watch" />
|
||||||
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
|
@ -34,7 +34,7 @@
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="@string/anime"
|
android:hint="@string/anime"
|
||||||
android:textColorHint="?attr/colorOnPrimaryContainer"
|
android:textColorHint="?attr/colorPrimary"
|
||||||
android:transitionName="@string/search"
|
android:transitionName="@string/search"
|
||||||
app:boxBackgroundColor="?attr/colorPrimaryContainer"
|
app:boxBackgroundColor="?attr/colorPrimaryContainer"
|
||||||
app:boxCornerRadiusBottomEnd="28dp"
|
app:boxCornerRadiusBottomEnd="28dp"
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
app:boxCornerRadiusTopEnd="28dp"
|
app:boxCornerRadiusTopEnd="28dp"
|
||||||
app:boxCornerRadiusTopStart="28dp"
|
app:boxCornerRadiusTopStart="28dp"
|
||||||
app:endIconDrawable="@drawable/ic_round_search_24"
|
app:endIconDrawable="@drawable/ic_round_search_24"
|
||||||
app:endIconTint="?attr/colorOnPrimaryContainer"
|
app:endIconTint="?attr/colorPrimary"
|
||||||
app:boxStrokeColor="@color/text_input_layout_stroke_color"
|
app:boxStrokeColor="@color/text_input_layout_stroke_color"
|
||||||
app:hintAnimationEnabled="true">
|
app:hintAnimationEnabled="true">
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
android:layout_width="52dp"
|
android:layout_width="52dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:backgroundTint="?attr/colorPrimaryContainer"
|
app:cardBackgroundColor="?attr/colorPrimaryContainer"
|
||||||
app:strokeColor="@color/text_input_layout_stroke_color"
|
app:strokeColor="@color/text_input_layout_stroke_color"
|
||||||
app:cardCornerRadius="26dp">
|
app:cardCornerRadius="26dp">
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
android:layout_width="52dp"
|
android:layout_width="52dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
android:tint="?attr/colorOnPrimaryContainer"
|
android:tint="?attr/colorPrimary"
|
||||||
app:srcCompat="@drawable/ic_round_settings_24"
|
app:srcCompat="@drawable/ic_round_settings_24"
|
||||||
tools:ignore="ContentDescription,ImageContrastCheck" />
|
tools:ignore="ContentDescription,ImageContrastCheck" />
|
||||||
|
|
||||||
|
|
|
@ -82,19 +82,70 @@
|
||||||
android:textAllCaps="true"
|
android:textAllCaps="true"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
tools:ignore="LabelFor,TextContrastCheck,DuplicateSpeakableTextCheck" />
|
tools:ignore="LabelFor,TextContrastCheck,DuplicateSpeakableTextCheck" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/animeSourceSubscribe"
|
android:id="@+id/animeSourceSubscribe"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="48dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
app:srcCompat="@drawable/ic_round_notifications_none_24"
|
app:srcCompat="@drawable/ic_round_notifications_none_24"
|
||||||
app:tint="?attr/colorOnBackground"
|
app:tint="?attr/colorOnBackground"
|
||||||
tools:ignore="ContentDescription,ImageContrastCheck" />
|
tools:ignore="ContentDescription,ImageContrastCheck" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="Language"
|
||||||
|
app:boxCornerRadiusBottomEnd="8dp"
|
||||||
|
app:boxCornerRadiusBottomStart="8dp"
|
||||||
|
app:boxCornerRadiusTopEnd="8dp"
|
||||||
|
app:boxCornerRadiusTopStart="8dp"
|
||||||
|
app:hintAnimationEnabled="true"
|
||||||
|
app:boxStrokeColor="@color/text_input_layout_stroke_color"
|
||||||
|
app:startIconDrawable="@drawable/ic_round_source_24">
|
||||||
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/animeSourceLanguage"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fontFamily="@font/poppins_bold"
|
||||||
|
android:freezesText="false"
|
||||||
|
android:inputType="none"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:text="LANG"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:ignore="LabelFor,TextContrastCheck,DuplicateSpeakableTextCheck" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/animeSourceSettings"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
app:srcCompat="@drawable/ic_round_settings_24"
|
||||||
|
app:tint="?attr/colorOnBackground"
|
||||||
|
tools:ignore="ContentDescription,ImageContrastCheck" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
|
@ -21,6 +22,17 @@
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:text="Extension Name" />
|
android:text="Extension Name" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/settingsImageView"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:src="@drawable/ic_round_settings_24"
|
||||||
|
app:tint="?attr/colorOnBackground"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:contentDescription="Settings"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/closeTextView"
|
android:id="@+id/closeTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="@string/manga"
|
android:hint="@string/manga"
|
||||||
android:textColorHint="?attr/colorOnPrimaryContainer"
|
android:textColorHint="?attr/colorPrimary"
|
||||||
android:transitionName="@string/search"
|
android:transitionName="@string/search"
|
||||||
app:boxBackgroundColor="?attr/colorPrimaryContainer"
|
app:boxBackgroundColor="?attr/colorPrimaryContainer"
|
||||||
app:boxCornerRadiusBottomEnd="28dp"
|
app:boxCornerRadiusBottomEnd="28dp"
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
app:boxCornerRadiusTopEnd="28dp"
|
app:boxCornerRadiusTopEnd="28dp"
|
||||||
app:boxCornerRadiusTopStart="28dp"
|
app:boxCornerRadiusTopStart="28dp"
|
||||||
app:endIconDrawable="@drawable/ic_round_search_24"
|
app:endIconDrawable="@drawable/ic_round_search_24"
|
||||||
app:endIconTint="?attr/colorOnPrimaryContainer"
|
app:endIconTint="?attr/colorPrimary"
|
||||||
app:boxStrokeColor="@color/text_input_layout_stroke_color"
|
app:boxStrokeColor="@color/text_input_layout_stroke_color"
|
||||||
app:hintAnimationEnabled="true">
|
app:hintAnimationEnabled="true">
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
android:layout_width="52dp"
|
android:layout_width="52dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:backgroundTint="?attr/colorPrimaryContainer"
|
app:cardBackgroundColor="?attr/colorPrimaryContainer"
|
||||||
app:strokeColor="@color/text_input_layout_stroke_color"
|
app:strokeColor="@color/text_input_layout_stroke_color"
|
||||||
app:cardCornerRadius="26dp">
|
app:cardCornerRadius="26dp">
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
android:layout_width="52dp"
|
android:layout_width="52dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
android:tint="?attr/colorOnPrimaryContainer"
|
android:tint="?attr/colorPrimary"
|
||||||
app:srcCompat="@drawable/ic_round_settings_24"
|
app:srcCompat="@drawable/ic_round_settings_24"
|
||||||
tools:ignore="ContentDescription,ImageContrastCheck" />
|
tools:ignore="ContentDescription,ImageContrastCheck" />
|
||||||
|
|
||||||
|
|
|
@ -2,5 +2,4 @@
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/mono"/>
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.1 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.3 KiB |
|
@ -10,4 +10,6 @@
|
||||||
<color name="status">#54000000</color>
|
<color name="status">#54000000</color>
|
||||||
<color name="nav_status">#80000000</color>
|
<color name="nav_status">#80000000</color>
|
||||||
<color name="filler">#29FF6B08</color>
|
<color name="filler">#29FF6B08</color>
|
||||||
|
|
||||||
|
<color name="grey_nav">#E8222222</color>
|
||||||
</resources>
|
</resources>
|
|
@ -22,6 +22,8 @@
|
||||||
<color name="grey_60">#999999</color>
|
<color name="grey_60">#999999</color>
|
||||||
<color name="darkest_Black">#030201</color>
|
<color name="darkest_Black">#030201</color>
|
||||||
|
|
||||||
|
<color name="grey_nav">#E8EDEDED</color>
|
||||||
|
|
||||||
<!-- theme 1 -->
|
<!-- theme 1 -->
|
||||||
<color name="seed_1">#00658e</color>
|
<color name="seed_1">#00658e</color>
|
||||||
<color name="md_theme_light_1_primary">#00658E</color>
|
<color name="md_theme_light_1_primary">#00658E</color>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<item name="android:windowSplashScreenAnimationDuration" tools:targetApi="s">1000</item>
|
<item name="android:windowSplashScreenAnimationDuration" tools:targetApi="s">1000</item>
|
||||||
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/anim_splash</item>
|
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/anim_splash</item>
|
||||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
|
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
|
||||||
|
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/bg_black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Dantotsu" parent="Theme.Base">
|
<style name="Theme.Dantotsu" parent="Theme.Base">
|
||||||
|
|
8
app/src/main/res/xml/anime_preferences.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Define your preferences here. For example: -->
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:key="some_key"
|
||||||
|
android:title="Some Title"
|
||||||
|
android:defaultValue="true" />
|
||||||
|
</PreferenceScreen>
|