diff --git a/app/build.gradle b/app/build.gradle index 8e394e12..141d3b05 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,6 +65,7 @@ dependencies { implementation 'com.github.Blatzar:NiceHttp:0.4.3' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' + implementation 'androidx.preference:preference:1.2.1' // Glide ext.glide_version = '4.16.0' diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index 20a7f23e..158604c1 100644 Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index 73334e60..33ac52e7 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -85,14 +85,20 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - + val _bottomBar = findViewById(R.id.navbar) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val bottomBar = findViewById(R.id.navbar) - val backgroundDrawable = bottomBar.background as GradientDrawable + + val backgroundDrawable = _bottomBar.background as GradientDrawable val currentColor = backgroundDrawable.color?.defaultColor ?: 0 val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt() backgroundDrawable.setColor(semiTransparentColor) - bottomBar.background = backgroundDrawable + _bottomBar.background = backgroundDrawable + } + val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + .getBoolean("colorOverflow", false) + if (!colorOverflow) { + _bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray) + } diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt index 67ca2afa..89280bae 100644 --- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt +++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt @@ -3,6 +3,7 @@ package ani.dantotsu.aniyomi.anime.custom import android.app.Application import android.content.Context +import androidx.core.content.ContextCompat import ani.dantotsu.media.manga.MangaCache import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import tachiyomi.core.preference.PreferenceStore @@ -12,7 +13,11 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences +import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager +import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager import kotlinx.serialization.json.Json +import tachiyomi.domain.source.anime.service.AnimeSourceManager +import tachiyomi.domain.source.manga.service.MangaSourceManager import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingleton @@ -26,9 +31,11 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { NetworkHelper(app, get()) } addSingletonFactory { AnimeExtensionManager(app) } - addSingletonFactory { MangaExtensionManager(app) } + addSingletonFactory { AndroidAnimeSourceManager(app, get()) } + addSingletonFactory { AndroidMangaSourceManager(app, get()) } + val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) addSingleton(sharedPreferences) @@ -40,6 +47,11 @@ class AppModule(val app: Application) : InjektModule { } addSingletonFactory { MangaCache() } + + ContextCompat.getMainExecutor(app).execute { + get() + get() + } } } diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index feba6ac9..1117cf46 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -1,8 +1,11 @@ package ani.dantotsu.home +import android.content.Context import android.content.Intent +import android.graphics.Color import android.os.Handler import android.os.Looper +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -19,6 +22,7 @@ import ani.dantotsu.media.GenreActivity import ani.dantotsu.MediaPageTransformer import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist +import ani.dantotsu.currContext import ani.dantotsu.databinding.ItemAnimePageBinding import ani.dantotsu.loadData import ani.dantotsu.loadImage @@ -58,6 +62,16 @@ class AnimePageAdapter : RecyclerView.Adapter(R.id.animeUserAvatarContainer) materialCardView.setCardBackgroundColor(semiTransparentColor) + val typedValue = TypedValue() + currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) + val color = typedValue.data + + + val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false + if (!colorOverflow) { + textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt() + materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) + } binding.animeTitleContainer.updatePadding(top = statusBarHeight) diff --git a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt index 5422d1cb..7f91aaaf 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt @@ -1,8 +1,10 @@ package ani.dantotsu.home +import android.content.Context import android.content.Intent import android.os.Handler import android.os.Looper +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -19,6 +21,7 @@ import ani.dantotsu.media.GenreActivity import ani.dantotsu.MediaPageTransformer import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist +import ani.dantotsu.currContext import ani.dantotsu.databinding.ItemMangaPageBinding import ani.dantotsu.loadData import ani.dantotsu.loadImage @@ -53,10 +56,20 @@ class MangaPageAdapter : RecyclerView.Adapter(R.id.mangaSearchBar) val currentColor = textInputLayout.boxBackgroundColor - val semiTransparentColor= (currentColor and 0x00FFFFFF) or 0xA8000000.toInt() + val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt() textInputLayout.boxBackgroundColor = semiTransparentColor val materialCardView = holder.itemView.findViewById(R.id.mangaUserAvatarContainer) materialCardView.setCardBackgroundColor(semiTransparentColor) + val typedValue = TypedValue() + currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) + val color = typedValue.data + + + val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false + if (!colorOverflow) { + textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt() + materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) + } binding.mangaTitleContainer.updatePadding(top = statusBarHeight) diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt index 1842a8e6..203859a8 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt @@ -120,8 +120,8 @@ class MediaDetailsViewModel : ViewModel() { private val episodes = MutableLiveData>>(null) private val epsLoaded = mutableMapOf>() fun getEpisodes(): LiveData>> = episodes - suspend fun loadEpisodes(media: Media, i: Int) { - if (!epsLoaded.containsKey(i)) { + suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) { + if (!epsLoaded.containsKey(i) || invalidate) { epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return } episodes.postValue(epsLoaded) @@ -240,9 +240,9 @@ class MediaDetailsViewModel : ViewModel() { private val mangaChapters = MutableLiveData>>(null) private val mangaLoaded = mutableMapOf>() fun getMangaChapters(): LiveData>> = mangaChapters - suspend fun loadMangaChapters(media: Media, i: Int) { + suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) { logger("Loading Manga Chapters : $mangaLoaded") - if (!mangaLoaded.containsKey(i)) tryWithSuspend { + if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend { mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend } mangaChapters.postValue(mangaLoaded) diff --git a/app/src/main/java/ani/dantotsu/media/Selected.kt b/app/src/main/java/ani/dantotsu/media/Selected.kt index ddb30f58..9982db51 100644 --- a/app/src/main/java/ani/dantotsu/media/Selected.kt +++ b/app/src/main/java/ani/dantotsu/media/Selected.kt @@ -9,6 +9,7 @@ data class Selected( var chip: Int = 0, //var source: String = "", var sourceIndex: Int = 0, + var langIndex: Int = 0, var preferDub: Boolean = false, var server: String? = null, var video: Int = 0, diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt index feb75e7a..33dcefe5 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -1,6 +1,8 @@ package ani.dantotsu.media.anime import android.annotation.SuppressLint +import android.app.AlertDialog +import android.content.Context import android.content.Intent import android.net.Uri import android.util.TypedValue @@ -8,23 +10,38 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter +import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout +import android.widget.Toast import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.* import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.SourceSearchDialogFragment +import ani.dantotsu.parsers.AnimeSources +import ani.dantotsu.parsers.DynamicAnimeParser import ani.dantotsu.parsers.WatchSources +import ani.dantotsu.settings.ExtensionsActivity +import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import com.google.android.material.chip.Chip +import com.google.android.material.tabs.TabLayout +import com.google.android.material.textfield.TextInputLayout +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager +import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.lang.IndexOutOfBoundsException class AnimeWatchAdapter( private val media: Media, @@ -70,7 +87,8 @@ class AnimeWatchAdapter( } //Source Selection - val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } + var source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } + setLanguageList(media.selected!!.langIndex,source) if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { binding.animeSource.setText(watchSources.names[source]) watchSources[source].apply { @@ -92,11 +110,41 @@ class AnimeWatchAdapter( binding.animeSourceDubbed.isChecked = selectDub changing = false binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE + source = i + setLanguageList(0,i) } subscribeButton(false) - fragment.loadEpisodes(i) + fragment.loadEpisodes(i, false) } + binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ -> + // Check if 'extension' and 'selected' properties exist and are accessible + (watchSources[source] as? DynamicAnimeParser)?.let { ext -> + ext.sourceLanguage = i + fragment.onLangChange(i) + fragment.onSourceChange(media.selected!!.sourceIndex).apply { + binding.animeSourceTitle.text = showUserText + showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } + changing = true + binding.animeSourceDubbed.isChecked = selectDub + changing = false + binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE + setLanguageList(i, source) + } + subscribeButton(false) + fragment.loadEpisodes(media.selected!!.sourceIndex, true) + } ?: run { + } + } + + //settings + binding.animeSourceSettings.setOnClickListener { + (watchSources[source] as? DynamicAnimeParser)?.let { ext -> + fragment.openSettings(ext.extension) + } + } + + //Subscription subscribe = MediaDetailsActivity.PopImageButton( fragment.lifecycleScope, @@ -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 inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) { diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt index fc3893c2..0613eceb 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -1,11 +1,17 @@ package ani.dantotsu.media.anime import android.annotation.SuppressLint +import android.app.AlertDialog import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.Toast +import androidx.cardview.widget.CardView import androidx.core.math.MathUtils import androidx.core.view.updatePadding import androidx.fragment.app.Fragment @@ -13,24 +19,38 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager +import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.* import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.media.Media +import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.HAnimeSources +import ani.dantotsu.settings.ExtensionsActivity +import ani.dantotsu.settings.InstalledAnimeExtensionsFragment import ani.dantotsu.settings.PlayerSettings import ani.dantotsu.settings.UserInterfaceSettings +import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.navigationrail.NavigationRailView +import com.google.android.material.tabs.TabLayout +import com.google.android.material.textfield.TextInputLayout +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager +import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import kotlin.math.ceil import kotlin.math.max import kotlin.math.roundToInt @@ -214,6 +234,13 @@ class AnimeWatchFragment : Fragment() { return model.watchSources?.get(i)!! } + fun onLangChange(i: Int) { + val selected = model.loadSelected(media) + selected.langIndex = i + model.saveSelected(media.id, selected, requireActivity()) + media.selected = selected + } + fun onDubClicked(checked: Boolean) { val selected = model.loadSelected(media) model.watchSources?.get(selected.sourceIndex)?.selectDub = checked @@ -223,8 +250,8 @@ class AnimeWatchFragment : Fragment() { lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) } } - fun loadEpisodes(i: Int) { - lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) } + fun loadEpisodes(i: Int, invalidate: Boolean) { + lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) } } fun onIconPressed(viewType: Int, rev: Boolean) { @@ -262,45 +289,115 @@ class AnimeWatchFragment : Fragment() { else getString(R.string.unsubscribed_notification) ) } - - fun onEpisodeClick(i: String) { - model.continueMedia = false - model.saveSelected(media.id, media.selected!!, requireActivity()) - model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) - } - - @SuppressLint("NotifyDataSetChanged") - private fun reload() { - val selected = model.loadSelected(media) - - //Find latest episode for subscription - selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f - selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest - - model.saveSelected(media.id, selected, requireActivity()) - headerAdapter.handleEpisodes() - episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) - var arr: ArrayList = 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) ?: arr + fun openSettings(pkg: AnimeExtension.Installed){ + val changeUIVisibility: (Boolean) -> Unit = { show -> + val activity = requireActivity() as MediaDetailsActivity + val visibility = if (show) View.VISIBLE else View.GONE + activity.findViewById(R.id.mediaAppBar).visibility = visibility + activity.findViewById(R.id.mediaViewPager).visibility = visibility + activity.findViewById(R.id.mediaCover).visibility = visibility + activity.findViewById(R.id.mediaClose).visibility = visibility + try{ + activity.findViewById(R.id.mediaTab).visibility = visibility + }catch (e: ClassCastException){ + activity.findViewById(R.id.mediaTab).visibility = visibility + } + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = + if (show) View.GONE else View.VISIBLE + } + val allSettings = pkg.sources.filterIsInstance() + if (allSettings.isNotEmpty()) { + var selectedSetting = allSettings[0] + if (allSettings.size > 1) { + val names = allSettings.map { it.lang }.toTypedArray() + var selectedIndex = 0 + AlertDialog.Builder(requireContext()) + .setTitle("Select a Source") + .setSingleChoiceItems(names, selectedIndex) { _, which -> + selectedIndex = which + } + .setPositiveButton("OK") { dialog, _ -> + selectedSetting = allSettings[selectedIndex] + dialog.dismiss() + + // Move the fragment transaction here + val fragment = + AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){ + changeUIVisibility(true) + loadEpisodes(media.selected!!.sourceIndex, true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.cancel() + changeUIVisibility(true) + return@setNegativeButton + } + .show() + } else { + // If there's only one setting, proceed with the fragment transaction + val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){ + changeUIVisibility(true) + loadEpisodes(media.selected!!.sourceIndex, true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + + changeUIVisibility(false) + } else { + Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) + .show() } - episodeAdapter.arr = arr - episodeAdapter.updateType(style ?: uiSettings.animeDefaultView) - episodeAdapter.notifyItemRangeInserted(0, arr.size) } - override fun onDestroy() { - model.watchSources?.flushText() - super.onDestroy() - } + fun onEpisodeClick(i: String) { + model.continueMedia = false + model.saveSelected(media.id, media.selected!!, requireActivity()) + model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) + } - var state: Parcelable? = null + @SuppressLint("NotifyDataSetChanged") + private fun reload() { + val selected = model.loadSelected(media) + + //Find latest episode for subscription + selected.latest = + media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f + selected.latest = + media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest + + model.saveSelected(media.id, selected, requireActivity()) + headerAdapter.handleEpisodes() + episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) + var arr: ArrayList = 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) ?: arr + } + episodeAdapter.arr = arr + episodeAdapter.updateType(style ?: uiSettings.animeDefaultView) + episodeAdapter.notifyItemRangeInserted(0, arr.size) + } + + override fun onDestroy() { + model.watchSources?.flushText() + super.onDestroy() + } + + var state: Parcelable? = null override fun onResume() { super.onResume() binding.mediaInfoProgressBar.visibility = progress diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt index 3579d6b9..8b64057d 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt @@ -17,12 +17,17 @@ import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.SourceSearchDialogFragment +import ani.dantotsu.parsers.AnimeSources +import ani.dantotsu.parsers.DynamicAnimeParser +import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.MangaReadSources +import ani.dantotsu.parsers.MangaSources import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import com.google.android.material.chip.Chip import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import java.lang.IndexOutOfBoundsException class MangaReadAdapter( private val media: Media, @@ -50,10 +55,10 @@ class MangaReadAdapter( } //Source Selection - val source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } + var source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } + setLanguageList(media.selected!!.langIndex,source) if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) { binding.animeSource.setText(mangaReadSources.names[source]) - mangaReadSources[source].apply { binding.animeSourceTitle.text = showUserText showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } @@ -65,9 +70,34 @@ class MangaReadAdapter( fragment.onSourceChange(i).apply { binding.animeSourceTitle.text = showUserText showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } + source = i + setLanguageList(0,i) } subscribeButton(false) - fragment.loadChapters(i) + 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 @@ -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 inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt index c8a83cca..61af0bf5 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -1,11 +1,15 @@ package ani.dantotsu.media.manga import android.annotation.SuppressLint +import android.app.AlertDialog import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.Toast +import androidx.cardview.widget.CardView import androidx.core.math.MathUtils.clamp import androidx.core.view.updatePadding import androidx.fragment.app.Fragment @@ -13,20 +17,30 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager +import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.* import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog import ani.dantotsu.media.Media +import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.MangaParser import ani.dantotsu.parsers.MangaSources import ani.dantotsu.settings.UserInterfaceSettings +import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment +import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.navigationrail.NavigationRailView +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension +import eu.kanade.tachiyomi.extension.manga.model.MangaExtension +import eu.kanade.tachiyomi.source.ConfigurableSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.math.ceil @@ -185,8 +199,16 @@ open class MangaReadFragment : Fragment() { return model.mangaReadSources?.get(i)!! } - fun loadChapters(i: Int) { - lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) } + fun onLangChange(i: Int) { + val selected = model.loadSelected(media) + selected.langIndex = i + model.saveSelected(media.id, selected, requireActivity()) + media.selected = selected + } + + + fun loadChapters(i: Int, invalidate: Boolean) { + lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i, invalidate) } } fun onIconPressed(viewType: Int, rev: Boolean) { @@ -225,6 +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(R.id.mediaAppBar).visibility = visibility + activity.findViewById(R.id.mediaViewPager).visibility = visibility + activity.findViewById(R.id.mediaCover).visibility = visibility + activity.findViewById(R.id.mediaClose).visibility = visibility + try{ + activity.findViewById(R.id.mediaTab).visibility = visibility + }catch (e: ClassCastException){ + activity.findViewById(R.id.mediaTab).visibility = visibility + } + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = + if (show) View.GONE else View.VISIBLE + } + val allSettings = pkg.sources.filterIsInstance() + if (allSettings.isNotEmpty()) { + var selectedSetting = allSettings[0] + if (allSettings.size > 1) { + val names = allSettings.map { it.lang }.toTypedArray() + var selectedIndex = 0 + AlertDialog.Builder(requireContext()) + .setTitle("Select a Source") + .setSingleChoiceItems(names, selectedIndex) { _, which -> + selectedIndex = which + } + .setPositiveButton("OK") { dialog, _ -> + selectedSetting = allSettings[selectedIndex] + dialog.dismiss() + + // Move the fragment transaction here + val fragment = + MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ + changeUIVisibility(true) + loadChapters(media.selected!!.sourceIndex, true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.cancel() + changeUIVisibility(true) + return@setNegativeButton + } + .show() + } else { + // If there's only one setting, proceed with the fragment transaction + val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ + changeUIVisibility(true) + loadChapters(media.selected!!.sourceIndex, true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + + changeUIVisibility(false) + } else { + Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) + .show() + } + } + fun onMangaChapterClick(i: String) { model.continueMedia = false media.manga?.chapters?.get(i)?.let { diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 6a4a58a7..887e6608 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -18,6 +18,8 @@ import ani.dantotsu.currContext import ani.dantotsu.logger import ani.dantotsu.media.manga.ImageData import ani.dantotsu.media.manga.MangaCache +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.animesource.model.AnimeFilter import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimesPage @@ -62,6 +64,7 @@ class AniyomiAdapter { class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { val extension: AnimeExtension.Installed + var sourceLanguage = 0 init { this.extension = extension } @@ -71,7 +74,12 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { override val isDubAvailableSeparately = false override val isNSFW = extension.isNsfw override suspend fun loadEpisodes(animeLink: String, extra: Map?, sAnime: SAnime): List { - val source = extension.sources.first() + val source = try{ + extension.sources[sourceLanguage] + }catch (e: Exception){ + sourceLanguage = 0 + extension.sources[sourceLanguage] + } if (source is AnimeCatalogueSource) { try { val res = source.getEpisodeList(sAnime) @@ -91,7 +99,12 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { } override suspend fun loadVideoServers(episodeLink: String, extra: Map?, sEpisode: SEpisode): List { - val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList() + val source = try{ + extension.sources[sourceLanguage] + }catch (e: Exception){ + sourceLanguage = 0 + extension.sources[sourceLanguage] + } as? AnimeCatalogueSource ?: return emptyList() return try { val videos = source.getVideoList(sEpisode) @@ -108,8 +121,12 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { } override suspend fun search(query: String): List { - val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList() - + val source = try{ + extension.sources[sourceLanguage] + }catch (e: Exception){ + sourceLanguage = 0 + extension.sources[sourceLanguage] + } as? AnimeCatalogueSource ?: return emptyList() return try { val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first() convertAnimesPageToShowResponse(res) @@ -174,6 +191,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { val mangaCache = Injekt.get() val extension: MangaExtension.Installed + var sourceLanguage = 0 init { this.extension = extension } @@ -183,7 +201,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { override val isNSFW = extension.isNsfw override suspend fun loadChapters(mangaLink: String, extra: Map?, sManga: SManga): List { - 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 { val res = source.getChapterList(sManga) @@ -201,7 +224,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List { - 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 { try { @@ -321,7 +349,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { override suspend fun search(query: String): List { - val source = extension.sources.first() as? HttpSource ?: return emptyList() + val source = try{ + extension.sources[sourceLanguage] + }catch (e: Exception){ + sourceLanguage = 0 + extension.sources[sourceLanguage] + } as? HttpSource ?: return emptyList() return try { val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first() diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt index 9294bd3c..6a1bf640 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt @@ -1,5 +1,6 @@ package ani.dantotsu.settings +import android.app.AlertDialog import android.app.NotificationManager import android.content.Context import android.os.Bundle @@ -7,8 +8,10 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -17,10 +20,15 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.R import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding import ani.dantotsu.loadData +import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment +import com.google.android.material.tabs.TabLayout +import com.google.android.material.textfield.TextInputLayout import com.google.firebase.crashlytics.FirebaseCrashlytics +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension @@ -30,62 +38,129 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class InstalledAnimeExtensionsFragment : Fragment() { + + private var _binding: FragmentAnimeExtensionsBinding? = null private val binding get() = _binding!! private lateinit var extensionsRecyclerView: RecyclerView val skipIcons = loadData("skip_extension_icons") ?: false private val animeExtensionManager: AnimeExtensionManager = Injekt.get() private val extensionsAdapter = AnimeExtensionsAdapter({ pkg -> - if (isAdded) { // Check if the fragment is currently added to its activity - val context = requireContext() // Store context in a variable - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once + val allSettings = pkg.sources.filterIsInstance() + if (allSettings.isNotEmpty()) { + var selectedSetting = allSettings[0] + if (allSettings.size > 1) { + val names = allSettings.map { it.lang }.toTypedArray() + var selectedIndex = 0 + AlertDialog.Builder(requireContext()) + .setTitle("Select a Source") + .setSingleChoiceItems(names, selectedIndex) { _, which -> + selectedIndex = which + } + .setPositiveButton("OK") { dialog, _ -> + selectedSetting = allSettings[selectedIndex] + dialog.dismiss() - if (pkg.hasUpdate) { - animeExtensionManager.updateExtension(pkg) - .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread - .subscribe( - { installStep -> - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_round_sync_24) - .setContentTitle("Updating extension") - .setContentText("Step: $installStep") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - }, - { error -> - FirebaseCrashlytics.getInstance().recordException(error) - Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_ERROR - ) - .setSmallIcon(R.drawable.ic_round_info_24) - .setContentTitle("Update failed: ${error.message}") - .setContentText("Error: ${error.message}") - .setPriority(NotificationCompat.PRIORITY_HIGH) - notificationManager.notify(1, builder.build()) - }, - { - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) - .setContentTitle("Update complete") - .setContentText("The extension has been successfully updated.") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) + // Move the fragment transaction here + val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){ + val activity = requireActivity() as ExtensionsActivity + activity.findViewById(R.id.viewPager).visibility = View.VISIBLE + activity.findViewById(R.id.tabLayout).visibility = View.VISIBLE + activity.findViewById(R.id.searchView).visibility = View.VISIBLE + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = + View.GONE } - ) + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.cancel() + return@setNegativeButton + } + .show() } else { - animeExtensionManager.uninstallExtension(pkg.pkgName) + // If there's only one setting, proceed with the fragment transaction + val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){ + val activity = requireActivity() as ExtensionsActivity + activity.findViewById(R.id.viewPager).visibility = View.VISIBLE + activity.findViewById(R.id.tabLayout).visibility = View.VISIBLE + activity.findViewById(R.id.searchView).visibility = View.VISIBLE + activity.findViewById(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(R.id.viewPager).visibility = View.GONE + activity.findViewById(R.id.tabLayout).visibility = View.GONE + activity.findViewById(R.id.searchView).visibility = View.GONE + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = View.VISIBLE + } else { + Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) + .show() } - }, skipIcons) + }, + { pkg -> + if (isAdded) { // Check if the fragment is currently added to its activity + val context = requireContext() // Store context in a variable + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once + + if (pkg.hasUpdate) { + animeExtensionManager.updateExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Updating extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + FirebaseCrashlytics.getInstance().recordException(error) + Log.e("AnimeExtensionsAdapter", "Error: ", error) // Log the error + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Update failed: ${error.message}") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + }, + { + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) + .setContentTitle("Update complete") + .setContentText("The extension has been successfully updated.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + } + ) + } else { + animeExtensionManager.uninstallExtension(pkg.pkgName) + } + } + }, skipIcons + ) override fun onCreateView( inflater: LayoutInflater, @@ -114,6 +189,7 @@ class InstalledAnimeExtensionsFragment : Fragment() { private class AnimeExtensionsAdapter( + private val onSettingsClicked: (AnimeExtension.Installed) -> Unit, private val onUninstallClicked: (AnimeExtension.Installed) -> Unit, skipIcons: Boolean ) : ListAdapter( @@ -152,10 +228,14 @@ class InstalledAnimeExtensionsFragment : Fragment() { holder.closeTextView.setOnClickListener { onUninstallClicked(extension) } + holder.settingsImageView.setOnClickListener { + onSettingsClicked(extension) + } } inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) + val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) val closeTextView: TextView = view.findViewById(R.id.closeTextView) } @@ -180,5 +260,4 @@ class InstalledAnimeExtensionsFragment : Fragment() { } } - } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt index 42dd191e..51e0487d 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt @@ -1,6 +1,7 @@ package ani.dantotsu.settings +import android.app.AlertDialog import android.app.NotificationManager import android.content.Context import android.os.Bundle @@ -8,8 +9,10 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -18,13 +21,18 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.R import ani.dantotsu.databinding.FragmentMangaExtensionsBinding import ani.dantotsu.loadData +import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment +import com.google.android.material.tabs.TabLayout +import com.google.android.material.textfield.TextInputLayout import com.google.firebase.crashlytics.FirebaseCrashlytics import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.model.MangaExtension +import eu.kanade.tachiyomi.source.ConfigurableSource import kotlinx.coroutines.launch import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt @@ -37,6 +45,66 @@ class InstalledMangaExtensionsFragment : Fragment() { val skipIcons = loadData("skip_extension_icons") ?: false private val mangaExtensionManager: MangaExtensionManager = Injekt.get() private val extensionsAdapter = MangaExtensionsAdapter({ pkg -> + val changeUIVisibility: (Boolean) -> Unit = { show -> + val activity = requireActivity() as ExtensionsActivity + val visibility = if (show) View.VISIBLE else View.GONE + activity.findViewById(R.id.viewPager).visibility = visibility + activity.findViewById(R.id.tabLayout).visibility = visibility + activity.findViewById(R.id.searchView).visibility = visibility + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = + if (show) View.GONE else View.VISIBLE + } + val allSettings = pkg.sources.filterIsInstance() + if (allSettings.isNotEmpty()) { + var selectedSetting = allSettings[0] + if (allSettings.size > 1) { + val names = allSettings.map { it.lang }.toTypedArray() + var selectedIndex = 0 + AlertDialog.Builder(requireContext()) + .setTitle("Select a Source") + .setSingleChoiceItems(names, selectedIndex) { _, which -> + selectedIndex = which + } + .setPositiveButton("OK") { dialog, _ -> + selectedSetting = allSettings[selectedIndex] + dialog.dismiss() + + // Move the fragment transaction here + val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ + changeUIVisibility(true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.cancel() + changeUIVisibility(true) + return@setNegativeButton + } + .show() + } else { + // If there's only one setting, proceed with the fragment transaction + val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ + changeUIVisibility(true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + + // Hide ViewPager2 and TabLayout + changeUIVisibility(false) + } else { + Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) + .show() + } + }, + { pkg -> if (isAdded) { // Check if the fragment is currently added to its activity val context = requireContext() // Store context in a variable val notificationManager = @@ -115,6 +183,7 @@ class InstalledMangaExtensionsFragment : Fragment() { private class MangaExtensionsAdapter( + private val onSettingsClicked: (MangaExtension.Installed) -> Unit, private val onUninstallClicked: (MangaExtension.Installed) -> Unit, skipIcons: Boolean ) : ListAdapter( @@ -153,10 +222,14 @@ class InstalledMangaExtensionsFragment : Fragment() { holder.closeTextView.setOnClickListener { onUninstallClicked(extension) } + holder.settingsImageView.setOnClickListener { + onSettingsClicked(extension) + } } inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) + val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) val closeTextView: TextView = view.findViewById(R.id.closeTextView) } diff --git a/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt b/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt new file mode 100644 index 00000000..b4f93561 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt @@ -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().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" + } +} diff --git a/app/src/main/java/ani/dantotsu/settings/extensionprefs/MangaPreferenceFragmentCompat.kt b/app/src/main/java/ani/dantotsu/settings/extensionprefs/MangaPreferenceFragmentCompat.kt new file mode 100644 index 00000000..d571ef77 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/extensionprefs/MangaPreferenceFragmentCompat.kt @@ -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().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" + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/UnmeteredSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/UnmeteredSource.kt new file mode 100644 index 00000000..840a223a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/UnmeteredSource.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt new file mode 100644 index 00000000..ae11e587 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt @@ -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?): MutableSet? { + return prefs.getStringSet(key, defValues) + } + + override fun putStringSet(key: String?, values: MutableSet?) { + prefs.edit { + putStringSet(key, values) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/anime/AndroidAnimeSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/anime/AndroidAnimeSourceManager.kt new file mode 100644 index 00000000..79b30b9e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/anime/AndroidAnimeSourceManager.kt @@ -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()) + + private val stubSourcesMap = ConcurrentHashMap() + + override val catalogueSources: Flow> = sourcesMapFlow.map { it.values.filterIsInstance() } + + init { + scope.launch { + extensionManager.installedExtensionsFlow + .collectLatest { extensions -> + val mutableMap = ConcurrentHashMap( + 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() + + override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance() + + override fun getStubSources(): List { + 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, "", "")) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceExtensions.kt new file mode 100644 index 00000000..a359d765 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceExtensions.kt @@ -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().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() + 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/manga/AndroidMangaSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/manga/AndroidMangaSourceManager.kt new file mode 100644 index 00000000..08bd2b61 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/manga/AndroidMangaSourceManager.kt @@ -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()) + + private val stubSourcesMap = ConcurrentHashMap() + + override val catalogueSources: Flow> = sourcesMapFlow.map { it.values.filterIsInstance() } + + init { + scope.launch { + extensionManager.installedExtensionsFlow + .collectLatest { extensions -> + val mutableMap = ConcurrentHashMap( + 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() + + override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance() + + override fun getStubSources(): List { + 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, "", "")) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceExtensions.kt new file mode 100644 index 00000000..afd17cea --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceExtensions.kt @@ -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().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() + 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt new file mode 100644 index 00000000..8e3141a2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -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 { + 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" +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt new file mode 100644 index 00000000..4428fb9a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt @@ -0,0 +1,10 @@ +@file:Suppress("PackageDirectoryMismatch") + +package androidx.preference + +/** + * Returns package-private [EditTextPreference.getOnBindEditTextListener] + */ +fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? { + return onBindEditTextListener +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiTextInputEditText.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiTextInputEditText.kt new file mode 100644 index 00000000..272ac155 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiTextInputEditText.kt @@ -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().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) + } + } +} diff --git a/app/src/main/java/tachiyomi/core/metadata/tachiyomi/AnimeDetails.kt b/app/src/main/java/tachiyomi/core/metadata/tachiyomi/AnimeDetails.kt new file mode 100644 index 00000000..414d4873 --- /dev/null +++ b/app/src/main/java/tachiyomi/core/metadata/tachiyomi/AnimeDetails.kt @@ -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? = null, + val status: Int? = null, +) diff --git a/app/src/main/java/tachiyomi/core/metadata/tachiyomi/MangaDetails.kt b/app/src/main/java/tachiyomi/core/metadata/tachiyomi/MangaDetails.kt new file mode 100644 index 00000000..7768986e --- /dev/null +++ b/app/src/main/java/tachiyomi/core/metadata/tachiyomi/MangaDetails.kt @@ -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? = null, + val status: Int? = null, +) diff --git a/app/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt b/app/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt new file mode 100644 index 00000000..f56bfbfd --- /dev/null +++ b/app/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt @@ -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() +} diff --git a/app/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt b/app/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt new file mode 100644 index 00000000..ede00f27 --- /dev/null +++ b/app/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt @@ -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?, + 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, + ) + } +} diff --git a/app/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt b/app/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt new file mode 100644 index 00000000..354abffc --- /dev/null +++ b/app/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt @@ -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?, + 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, + ) + } +} diff --git a/app/src/main/java/tachiyomi/domain/items/episode/service/EpisodeRecognition.kt b/app/src/main/java/tachiyomi/domain/items/episode/service/EpisodeRecognition.kt new file mode 100644 index 00000000..c40410a3 --- /dev/null +++ b/app/src/main/java/tachiyomi/domain/items/episode/service/EpisodeRecognition.kt @@ -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 + } +} diff --git a/app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSource.kt b/app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSource.kt new file mode 100644 index 00000000..182e9653 --- /dev/null +++ b/app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSource.kt @@ -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" + } + } +} diff --git a/app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSourceWithCount.kt b/app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSourceWithCount.kt new file mode 100644 index 00000000..65b928bc --- /dev/null +++ b/app/src/main/java/tachiyomi/domain/source/anime/model/AnimeSourceWithCount.kt @@ -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 +} diff --git a/app/src/main/java/tachiyomi/domain/source/anime/model/Pin.kt b/app/src/main/java/tachiyomi/domain/source/anime/model/Pin.kt new file mode 100644 index 00000000..e4430d3a --- /dev/null +++ b/app/src/main/java/tachiyomi/domain/source/anime/model/Pin.kt @@ -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) + } +} diff --git a/app/src/main/java/tachiyomi/domain/source/anime/model/StubAnimeSource.kt b/app/src/main/java/tachiyomi/domain/source/anime/model/StubAnimeSource.kt new file mode 100644 index 00000000..7631c49c --- /dev/null +++ b/app/src/main/java/tachiyomi/domain/source/anime/model/StubAnimeSource.kt @@ -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 { + throw AnimeSourceNotInstalledException() + } + + override suspend fun getVideoList(episode: SEpisode): List diff --git a/app/src/main/res/layout/activity_media.xml b/app/src/main/res/layout/activity_media.xml index 844524a2..a6b47cdf 100644 --- a/app/src/main/res/layout/activity_media.xml +++ b/app/src/main/res/layout/activity_media.xml @@ -293,4 +293,12 @@ tools:ignore="ContentDescription,ImageContrastCheck" tools:srcCompat="@tools:sample/backgrounds/scenic[2]" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_anime_watch.xml b/app/src/main/res/layout/fragment_anime_watch.xml index 54d0545b..2a65e1ea 100644 --- a/app/src/main/res/layout/fragment_anime_watch.xml +++ b/app/src/main/res/layout/fragment_anime_watch.xml @@ -39,4 +39,6 @@ android:paddingBottom="128dp" tools:itemCount="1" tools:listitem="@layout/item_anime_watch" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_anime_page.xml b/app/src/main/res/layout/item_anime_page.xml index 0c5ecc09..ad473996 100644 --- a/app/src/main/res/layout/item_anime_page.xml +++ b/app/src/main/res/layout/item_anime_page.xml @@ -34,7 +34,7 @@ android:layout_marginEnd="8dp" android:layout_weight="1" android:hint="@string/anime" - android:textColorHint="?attr/colorOnPrimaryContainer" + android:textColorHint="?attr/colorPrimary" android:transitionName="@string/search" app:boxBackgroundColor="?attr/colorPrimaryContainer" app:boxCornerRadiusBottomEnd="28dp" @@ -42,7 +42,7 @@ app:boxCornerRadiusTopEnd="28dp" app:boxCornerRadiusTopStart="28dp" app:endIconDrawable="@drawable/ic_round_search_24" - app:endIconTint="?attr/colorOnPrimaryContainer" + app:endIconTint="?attr/colorPrimary" app:boxStrokeColor="@color/text_input_layout_stroke_color" app:hintAnimationEnabled="true"> @@ -64,7 +64,7 @@ android:layout_width="52dp" android:layout_height="match_parent" android:layout_marginTop="4dp" - android:backgroundTint="?attr/colorPrimaryContainer" + app:cardBackgroundColor="?attr/colorPrimaryContainer" app:strokeColor="@color/text_input_layout_stroke_color" app:cardCornerRadius="26dp"> @@ -73,7 +73,7 @@ android:layout_width="52dp" android:layout_height="52dp" android:scaleType="center" - android:tint="?attr/colorOnPrimaryContainer" + android:tint="?attr/colorPrimary" app:srcCompat="@drawable/ic_round_settings_24" tools:ignore="ContentDescription,ImageContrastCheck" /> diff --git a/app/src/main/res/layout/item_anime_watch.xml b/app/src/main/res/layout/item_anime_watch.xml index 9be21673..bea04864 100644 --- a/app/src/main/res/layout/item_anime_watch.xml +++ b/app/src/main/res/layout/item_anime_watch.xml @@ -82,19 +82,70 @@ android:textAllCaps="true" android:textColor="?android:attr/textColorSecondary" android:textSize="14sp" + android:ellipsize="end" + android:maxLines="1" tools:ignore="LabelFor,TextContrastCheck,DuplicateSpeakableTextCheck" /> + + + + + + + + + + + + @@ -21,6 +22,17 @@ android:textSize="18sp" android:text="Extension Name" /> + + @@ -66,7 +66,7 @@ android:layout_width="52dp" android:layout_height="match_parent" android:layout_marginTop="4dp" - android:backgroundTint="?attr/colorPrimaryContainer" + app:cardBackgroundColor="?attr/colorPrimaryContainer" app:strokeColor="@color/text_input_layout_stroke_color" app:cardCornerRadius="26dp"> @@ -75,7 +75,7 @@ android:layout_width="52dp" android:layout_height="52dp" android:scaleType="center" - android:tint="?attr/colorOnPrimaryContainer" + android:tint="?attr/colorPrimary" app:srcCompat="@drawable/ic_round_settings_24" tools:ignore="ContentDescription,ImageContrastCheck" /> diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 636da759..bbd3e021 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,4 @@ - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..feb696da Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 2aafff77..feb696da 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..c30ada76 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 1c7e6529..c30ada76 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..380a6b31 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 4dcbec76..380a6b31 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..3276f97b Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 89d1411f..3276f97b 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..96a88d74 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 5a7049b5..96a88d74 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 037fa6f4..d0c07b09 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -10,4 +10,6 @@ #54000000 #80000000 #29FF6B08 + + #E8222222 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8c98f79e..7eaa3516 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -22,6 +22,8 @@ #999999 #030201 + #E8EDEDED + #00658e #00658E diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a1e9864b..0058aae7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -18,6 +18,7 @@ 1000 @drawable/anim_splash shortEdges + @color/bg_black