extension settings

This commit is contained in:
Finnley Somdahl 2023-10-29 19:45:11 -05:00
parent 9c0ef7a788
commit 3368a1bc8d
76 changed files with 2320 additions and 131 deletions

View file

@ -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'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -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)
} }

View file

@ -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>()
}
} }
} }

View file

@ -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)

View file

@ -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
@ -53,10 +56,20 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar) val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
val currentColor = textInputLayout.boxBackgroundColor val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor= (currentColor and 0x00FFFFFF) or 0xA8000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
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)

View file

@ -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)

View file

@ -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,

View file

@ -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) {

View file

@ -1,11 +1,17 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -13,24 +19,38 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.InstalledAnimeExtensionsFragment
import ani.dantotsu.settings.PlayerSettings import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -214,6 +234,13 @@ class AnimeWatchFragment : Fragment() {
return model.watchSources?.get(i)!! return model.watchSources?.get(i)!!
} }
fun onLangChange(i: Int) {
val selected = model.loadSelected(media)
selected.langIndex = i
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
}
fun onDubClicked(checked: Boolean) { fun onDubClicked(checked: Boolean) {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
@ -223,8 +250,8 @@ class AnimeWatchFragment : Fragment() {
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) } lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
} }
fun loadEpisodes(i: Int) { fun loadEpisodes(i: Int, invalidate: Boolean) {
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) } lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
} }
fun onIconPressed(viewType: Int, rev: Boolean) { fun onIconPressed(viewType: Int, rev: Boolean) {
@ -262,45 +289,115 @@ class AnimeWatchFragment : Fragment() {
else getString(R.string.unsubscribed_notification) else getString(R.string.unsubscribed_notification)
) )
} }
fun openSettings(pkg: AnimeExtension.Installed){
fun onEpisodeClick(i: String) { val changeUIVisibility: (Boolean) -> Unit = { show ->
model.continueMedia = false val activity = requireActivity() as MediaDetailsActivity
model.saveSelected(media.id, media.selected!!, requireActivity()) val visibility = if (show) View.VISIBLE else View.GONE
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
} activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
@SuppressLint("NotifyDataSetChanged") activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
private fun reload() { try{
val selected = model.loadSelected(media) activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
}catch (e: ClassCastException){
//Find latest episode for subscription activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f }
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
model.saveSelected(media.id, selected, requireActivity()) }
headerAdapter.handleEpisodes() val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) if (allSettings.isNotEmpty()) {
var arr: ArrayList<Episode> = arrayListOf() var selectedSetting = allSettings[0]
if (media.anime!!.episodes != null) { if (allSettings.size > 1) {
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null val names = allSettings.map { it.lang }.toTypedArray()
arr.addAll( var selectedIndex = 0
media.anime!!.episodes!!.values.toList() AlertDialog.Builder(requireContext())
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1))) .setTitle("Select a Source")
) .setSingleChoiceItems(names, selectedIndex) { _, which ->
if (reverse) selectedIndex = which
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr }
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
// Move the fragment transaction here
val fragment =
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
changeUIVisibility(true)
return@setNegativeButton
}
.show()
} else {
// If there's only one setting, proceed with the fragment transaction
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
} }
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
} }
override fun onDestroy() { fun onEpisodeClick(i: String) {
model.watchSources?.flushText() model.continueMedia = false
super.onDestroy() model.saveSelected(media.id, media.selected!!, requireActivity())
} model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
}
var state: Parcelable? = null @SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
//Find latest episode for subscription
selected.latest =
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes()
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) {
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
arr.addAll(
media.anime!!.episodes!!.values.toList()
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
)
if (reverse)
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
}
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
}
override fun onDestroy() {
model.watchSources?.flushText()
super.onDestroy()
}
var state: Parcelable? = null
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress

View file

@ -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)

View file

@ -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 {

View file

@ -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()

View file

@ -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,62 +38,129 @@ 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 ->
if (isAdded) { // Check if the fragment is currently added to its activity val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
val context = requireContext() // Store context in a variable if (allSettings.isNotEmpty()) {
val notificationManager = var selectedSetting = allSettings[0]
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
var selectedIndex = 0
AlertDialog.Builder(requireContext())
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
if (pkg.hasUpdate) { // Move the fragment transaction here
animeExtensionManager.updateExtension(pkg) val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread val activity = requireActivity() as ExtensionsActivity
.subscribe( activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.VISIBLE
{ installStep -> activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.VISIBLE
val builder = NotificationCompat.Builder( activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.VISIBLE
context, activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
Notifications.CHANNEL_DOWNLOADER_PROGRESS View.GONE
)
.setSmallIcon(R.drawable.ic_round_sync_24)
.setContentTitle("Updating extension")
.setContentText("Step: $installStep")
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
},
{ error ->
FirebaseCrashlytics.getInstance().recordException(error)
Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_ERROR
)
.setSmallIcon(R.drawable.ic_round_info_24)
.setContentTitle("Update failed: ${error.message}")
.setContentText("Error: ${error.message}")
.setPriority(NotificationCompat.PRIORITY_HIGH)
notificationManager.notify(1, builder.build())
},
{
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
.setContentTitle("Update complete")
.setContentText("The extension has been successfully updated.")
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
} }
) 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 { } else {
animeExtensionManager.uninstallExtension(pkg.pkgName) // If there's only one setting, proceed with the fragment transaction
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
val activity = requireActivity() as ExtensionsActivity
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.VISIBLE
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.VISIBLE
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.VISIBLE
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
View.GONE
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
} }
// Hide ViewPager2 and TabLayout
val activity = requireActivity() as ExtensionsActivity
activity.findViewById<ViewPager2>(R.id.viewPager).visibility = View.GONE
activity.findViewById<TabLayout>(R.id.tabLayout).visibility = View.GONE
activity.findViewById<TextInputLayout>(R.id.searchView).visibility = View.GONE
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = View.VISIBLE
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
} }
}, skipIcons) },
{ pkg ->
if (isAdded) { // Check if the fragment is currently added to its activity
val context = requireContext() // Store context in a variable
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once
if (pkg.hasUpdate) {
animeExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
.subscribe(
{ installStep ->
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(R.drawable.ic_round_sync_24)
.setContentTitle("Updating extension")
.setContentText("Step: $installStep")
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
},
{ error ->
FirebaseCrashlytics.getInstance().recordException(error)
Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_ERROR
)
.setSmallIcon(R.drawable.ic_round_info_24)
.setContentTitle("Update failed: ${error.message}")
.setContentText("Error: ${error.message}")
.setPriority(NotificationCompat.PRIORITY_HIGH)
notificationManager.notify(1, builder.build())
},
{
val builder = NotificationCompat.Builder(
context,
Notifications.CHANNEL_DOWNLOADER_PROGRESS
)
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
.setContentTitle("Update complete")
.setContentText("The extension has been successfully updated.")
.setPriority(NotificationCompat.PRIORITY_LOW)
notificationManager.notify(1, builder.build())
}
)
} else {
animeExtensionManager.uninstallExtension(pkg.pkgName)
}
}
}, skipIcons
)
override fun onCreateView( 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() {
} }
} }
} }

View file

@ -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)
} }

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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, "", ""))
}
}

View file

@ -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

View file

@ -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, "", ""))
}
}

View file

@ -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

View 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"
}

View file

@ -0,0 +1,10 @@
@file:Suppress("PackageDirectoryMismatch")
package androidx.preference
/**
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
*/
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
return onBindEditTextListener
}

View file

@ -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)
}
}
}

View file

@ -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,
)

View file

@ -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,
)

View 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()
}

View 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,
)
}
}

View 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,
)
}
}

View file

@ -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
}
}

View file

@ -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"
}
}
}

View file

@ -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
}

View 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)
}
}

View file

@ -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()

View file

@ -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
}

View file

@ -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>
}

View file

@ -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()

View file

@ -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>
}

View file

@ -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

View file

@ -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

View file

@ -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))
}

View file

@ -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))
}

View 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
}
}

View 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>

View 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>

View file

@ -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>

View 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>

View file

@ -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

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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" />

View file

@ -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"

View file

@ -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"

View file

@ -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" />

View file

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Before After
Before After

View file

@ -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>

View file

@ -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>

View file

@ -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">

View 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>