From 0bacfb8494290396af7070114f8380df16b35d35 Mon Sep 17 00:00:00 2001 From: aayush262 <99584765+aayush2622@users.noreply.github.com> Date: Wed, 20 Dec 2023 03:42:15 +0530 Subject: [PATCH 01/29] incognito view fix (#89) * auto pip * undo --- .../ani/dantotsu/home/AnimePageAdapter.kt | 4 ++- .../java/ani/dantotsu/home/HomeFragment.kt | 4 ++- .../ani/dantotsu/home/MangaPageAdapter.kt | 4 ++- app/src/main/res/layout/fragment_home.xml | 27 +++++++------------ app/src/main/res/layout/item_anime_page.xml | 9 +------ app/src/main/res/layout/item_anime_watch.xml | 1 + app/src/main/res/layout/item_manga_page.xml | 9 +------ 7 files changed, 21 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index 4e5b23ad..19b1002d 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -79,7 +79,9 @@ class AnimePageAdapter : RecyclerView.Adapter - - - - diff --git a/app/src/main/res/layout/item_anime_page.xml b/app/src/main/res/layout/item_anime_page.xml index 7e3b52ba..ef29558a 100644 --- a/app/src/main/res/layout/item_anime_page.xml +++ b/app/src/main/res/layout/item_anime_page.xml @@ -8,13 +8,6 @@ android:layout_marginStart="-16dp" android:layout_marginEnd="-16dp" android:orientation="vertical"> - - Date: Wed, 20 Dec 2023 08:02:29 +0600 Subject: [PATCH 02/29] Added mismatched switch (#91) Ayushs fault --- .../main/res/layout/activity_user_interface_settings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/activity_user_interface_settings.xml b/app/src/main/res/layout/activity_user_interface_settings.xml index ed878152..bc3be015 100644 --- a/app/src/main/res/layout/activity_user_interface_settings.xml +++ b/app/src/main/res/layout/activity_user_interface_settings.xml @@ -121,7 +121,7 @@ - - + - \ No newline at end of file + From eb75d299d2787e712f5224abc65bfd8e6f2bb1d7 Mon Sep 17 00:00:00 2001 From: aayush262 <99584765+aayush2622@users.noreply.github.com> Date: Tue, 26 Dec 2023 09:19:34 +0530 Subject: [PATCH 03/29] Bug fixes , download page redesign , new theme(Emerald) (#95) * Restart option when choosing custom theme Typo fix Extension page bug fix * Downloaded manga page redesign(lol) * quick fix * New theme(Emerald) Fine-tuned colors.xml * Toggle for list view and compact view in downloaded manga and novels (much more) --- .../download/manga/OfflineMangaAdapter.kt | 36 +- .../download/manga/OfflineMangaFragment.kt | 63 ++- .../download/manga/OfflineMangaModel.kt | 7 +- .../java/ani/dantotsu/media/MediaAdaptor.kt | 2 +- .../InstalledAnimeExtensionsFragment.kt | 84 ++-- .../InstalledMangaExtensionsFragment.kt | 10 + .../ani/dantotsu/settings/SettingsActivity.kt | 18 +- .../settings/paging/AnimePagingSource.kt | 10 +- .../settings/paging/MangaPagingSource.kt | 10 +- .../java/ani/dantotsu/themes/ThemeManager.kt | 12 +- .../res/layout/fragment_manga_offline.xml | 65 ++- app/src/main/res/layout/item_media_large.xml | 36 ++ app/src/main/res/values-night/themes.xml | 259 +++++++----- app/src/main/res/values/colors.xml | 396 +++++++++++------- app/src/main/res/values/themes.xml | 203 +++++---- 15 files changed, 762 insertions(+), 449 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt index b91bb55a..f9beeaf3 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt @@ -1,11 +1,13 @@ package ani.dantotsu.download.manga +import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import androidx.cardview.widget.CardView import ani.dantotsu.R @@ -32,10 +34,18 @@ class OfflineMangaAdapter( return position.toLong() } + @SuppressLint("SetTextI18n") override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + + val style = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0) + var view = convertView - if (view == null) { - view = inflater.inflate(R.layout.item_media_compact, parent, false) + + if (view == null && style == 0 ) { + view = inflater.inflate(R.layout.item_media_large, parent, false) // large view + } + else if (view == null && style == 1){ + view = inflater.inflate(R.layout.item_media_compact, parent, false) // compact view } val item = getItem(position) as OfflineMangaModel @@ -44,11 +54,31 @@ class OfflineMangaAdapter( val itemScore = view.findViewById(R.id.itemCompactScore) val itemScoreBG = view.findViewById(R.id.itemCompactScoreBG) val ongoing = view.findViewById(R.id.itemCompactOngoing) + val totalchapter = view.findViewById(R.id.itemCompactTotal) + val type = view.findViewById(R.id.itemCompactRelation) + val typeView = view.findViewById(R.id.itemCompactType) + + if (style == 0){ + val bannerView = view.findViewById(R.id.itemCompactBanner) // for large view + val chapters = view.findViewById(R.id.itemTotal) + chapters.text = " Chapters" + bannerView.setImageURI(item.banner) + totalchapter.text = item.totalchapter + } + + else if (style == 1){ + val readchapter = view.findViewById(R.id.itemCompactUserProgress) // for compact view + readchapter.text = item.readchapter + totalchapter.text = " | "+item.totalchapter + } + // Bind item data to the views - // For example: + type.text = item.type + typeView.visibility = View.VISIBLE imageView.setImageURI(item.image) titleTextView.text = item.title itemScore.text = item.score + if (item.isOngoing) { ongoing.visibility = View.VISIBLE } else { diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt index e81d2817..a3d330a4 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -2,6 +2,7 @@ package ani.dantotsu.download.manga import android.animation.ObjectAnimator import android.content.Context +import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Build @@ -16,17 +17,23 @@ import android.view.ViewGroup import android.view.animation.OvershootInterpolator import android.widget.AutoCompleteTextView import android.widget.GridView +import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView +import androidx.core.app.ActivityCompat.recreate import androidx.fragment.app.Fragment import ani.dantotsu.R +import ani.dantotsu.Refresh +import ani.dantotsu.currActivity import ani.dantotsu.currContext import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.initActivity import ani.dantotsu.logger import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.setSafeOnClickListener +import ani.dantotsu.settings.SettingsActivity import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.snackString import ani.dantotsu.statusBarHeight @@ -45,6 +52,7 @@ import kotlin.math.max import kotlin.math.min class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { + private val downloadManager = Injekt.get() private var downloads: List = listOf() private lateinit var gridView: GridView @@ -91,15 +99,49 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { override fun afterTextChanged(s: Editable?) { } - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int, ) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { onSearchQuery(s.toString()) } }) + var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + ?.getInt("offline_view", 0) + val layoutList = view.findViewById(R.id.downloadedList) + val layoutcompact = view.findViewById(R.id.downloadedGrid) + var selected = when (style) { + 0 -> layoutList + 1 -> layoutcompact + else -> layoutList + } + selected.alpha = 1f - gridView = view.findViewById(R.id.gridView) + fun selected(it: ImageView) { + selected.alpha = 0.33f + selected = it + selected.alpha = 1f + } + + layoutList.setOnClickListener { + selected(it as ImageView) + style = 0 + context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit() + ?.putInt("offline_view", style!!)?.apply() + recreate(requireActivity()) + + } + + layoutcompact.setOnClickListener { + selected(it as ImageView) + style = 1 + context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit() + ?.putInt("offline_view", style!!)?.apply() + recreate(requireActivity()) + } + + gridView = if(style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1) + gridView.visibility = View.VISIBLE getDownloads() adapter = OfflineMangaAdapter(requireContext(), downloads, this) gridView.adapter = adapter @@ -286,20 +328,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { val cover = File(directory, "cover.jpg") val coverUri: Uri? = if (cover.exists()) { Uri.fromFile(cover) - } else { - null - } + } else null + val banner = File(directory, "banner.jpg") + val bannerUri: Uri? = if (banner.exists()) { + Uri.fromFile(banner) + } else null val title = mediaModel.nameMAL ?: mediaModel.nameRomaji val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore ?: 0) else mediaModel.userScore) / 10.0).toString() - val isOngoing = false + val isOngoing = mediaModel.status == currActivity()!!.getString(R.string.status_releasing) val isUserScored = mediaModel.userScore != 0 - return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri) + val readchapter = (mediaModel.userProgress ?: "~").toString() + val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}" + val chapters = " Chapters" + return OfflineMangaModel(title, score, totalchapter, readchapter, type, chapters, isOngoing, isUserScored, coverUri , bannerUri ) } catch (e: Exception) { logger("Error loading media.json: ${e.message}") logger(e.printStackTrace()) FirebaseCrashlytics.getInstance().recordException(e) - return OfflineMangaModel("unknown", "0", false, false, null) + return OfflineMangaModel("unknown", "0", "??", "??","movie" ,"hmm", false, false, null , null) } } } diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt index 568081ee..3cebc6f6 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt @@ -5,7 +5,12 @@ import android.net.Uri data class OfflineMangaModel( val title: String, val score: String, + val totalchapter: String, + val readchapter : String, + val type: String, + val chapters: String, val isOngoing: Boolean, val isUserScored: Boolean, - val image: Uri? + val image: Uri?, + val banner: Uri? ) \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt index fd04eb58..6483909f 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt @@ -136,7 +136,7 @@ class MediaAdaptor( val media = mediaList?.get(position) if (media != null) { b.itemCompactImage.loadImage(media.cover) - b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400) + b.itemCompactBanner.loadImage(media.banner ?: media.cover) b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactTitle.text = media.userPreferredName diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt index bdbcb7ca..49489825 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.annotation.SuppressLint import android.app.AlertDialog import android.app.NotificationManager import android.content.Context @@ -49,6 +50,19 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { private val animeExtensionManager: AnimeExtensionManager = Injekt.get() private val extensionsAdapter = AnimeExtensionsAdapter( { pkg -> + val name= pkg.name + 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.languageselect).visibility = visibility + activity.findViewById(R.id.extensions).text = if (show) getString(R.string.extensions) else name + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = + if (show) View.GONE else View.VISIBLE + } + var itemSelected = false val allSettings = pkg.sources.filterIsInstance() if (allSettings.isNotEmpty()) { var selectedSetting = allSettings[0] @@ -58,65 +72,44 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) .setTitle("Select a Source") .setSingleChoiceItems(names, selectedIndex) { dialog, which -> + itemSelected = true selectedIndex = which selectedSetting = allSettings[selectedIndex] dialog.dismiss() - // Move the fragment transaction here - val eActivity = requireActivity() as ExtensionsActivity - eActivity.runOnUiThread { - val fragment = - AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { - - eActivity.findViewById(R.id.viewPager).visibility = - View.VISIBLE - eActivity.findViewById(R.id.tabLayout).visibility = - View.VISIBLE - eActivity.findViewById(R.id.searchView).visibility = - View.VISIBLE - eActivity.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() + val fragment = + AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { + changeUIVisibility(true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } + .setOnDismissListener { + if (!itemSelected) { + changeUIVisibility(true) } } .show() dialog.window?.setDimAmount(0.8f) } else { // If there's only one setting, proceed with the fragment transaction - val eActivity = requireActivity() as ExtensionsActivity - eActivity.runOnUiThread { - val fragment = - AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { + val fragment = + AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { + changeUIVisibility(true) + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() - eActivity.findViewById(R.id.viewPager).visibility = - View.VISIBLE - eActivity.findViewById(R.id.tabLayout).visibility = - View.VISIBLE - eActivity.findViewById(R.id.searchView).visibility = - View.VISIBLE - eActivity.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 + changeUIVisibility(false) } else { Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) .show() @@ -225,6 +218,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { return ViewHolder(view) } + @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: ViewHolder, position: Int) { val extension = getItem(position) // Use getItem() from ListAdapter val nsfw = if (extension.isNsfw) "(18+)" else "" diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt index ecef2e58..f31319d9 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt @@ -47,15 +47,19 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { val skipIcons = loadData("skip_extension_icons") ?: false private val mangaExtensionManager: MangaExtensionManager = Injekt.get() private val extensionsAdapter = MangaExtensionsAdapter({ pkg -> + val name= pkg.name 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.languageselect).visibility = visibility + activity.findViewById(R.id.extensions).text = if (show) getString(R.string.extensions) else name activity.findViewById(R.id.fragmentExtensionsContainer).visibility = if (show) View.GONE else View.VISIBLE } + var itemSelected = false val allSettings = pkg.sources.filterIsInstance() if (allSettings.isNotEmpty()) { var selectedSetting = allSettings[0] @@ -65,6 +69,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) .setTitle("Select a Source") .setSingleChoiceItems(names, selectedIndex) { dialog, which -> + itemSelected = true selectedIndex = which selectedSetting = allSettings[selectedIndex] dialog.dismiss() @@ -80,6 +85,11 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { .addToBackStack(null) .commit() } + .setOnDismissListener { + if (!itemSelected) { + changeUIVisibility(true) + } + } .show() dialog.window?.setDimAmount(0.8f) } else { diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index c037959a..d5255095 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -178,13 +178,18 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListen binding.customTheme.setOnClickListener { - val originalColor = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt( + val originalColor = getSharedPreferences("Dantotsu", MODE_PRIVATE).getInt( "custom_theme_int", Color.parseColor("#6200EE") ) + class CustomColorDialog : SimpleColorDialog() { //idk where to put it + override fun onPositiveButtonClick() { + restartApp() + super.onPositiveButtonClick() + } + } val tag = "colorPicker" - SimpleColorDialog.build() - .title("Custom Theme") + CustomColorDialog().title("Custom Theme") .colorPreset(originalColor) .colors(this, SimpleColorDialog.BEIGE_COLOR_PALLET) .allowCustom(true) @@ -251,9 +256,9 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListen binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked -> saveData("skip_extension_icons", isChecked) } - binding.NSFWExtension.isChecked = loadData("NFSWExtension") ?: true + binding.NSFWExtension.isChecked = loadData("NSFWExtension") ?: true binding.NSFWExtension.setOnCheckedChangeListener { _, isChecked -> - saveData("NFSWExtension", isChecked) + saveData("NSFWExtension", isChecked) } @@ -427,6 +432,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListen initActivity(this) } + binding.uiSettingsAnime.setOnClickListener { uiTheme(0, it) } @@ -793,4 +799,4 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListen show() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt index 045ca140..5ead2138 100644 --- a/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt +++ b/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt @@ -1,5 +1,6 @@ package ani.dantotsu.settings.paging +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import android.view.animation.LinearInterpolator @@ -91,18 +92,14 @@ class AnimeExtensionPagingSource( val availableExtensions = availableExtensionsFlow.filterNot { it.pkgName in installedExtensions } val query = searchQuery - val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true + val isNsfwEnabled: Boolean = loadData("NSFWExtension") ?: true val filteredExtensions = if (query.isEmpty()) { availableExtensions } else { availableExtensions.filter { it.name.contains(query, ignoreCase = true) } } - val filternfsw = if (isNsfwEnabled) { - filteredExtensions - } else { - filteredExtensions.filterNot { it.isNsfw } - } + val filternfsw = if (isNsfwEnabled) filteredExtensions else filteredExtensions.filterNot { it.isNsfw } return try { val sublist = filternfsw.subList( fromIndex = position, @@ -198,6 +195,7 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen val extensionIconImageView: ImageView = binding.extensionIconImageView + @SuppressLint("SetTextI18n") fun bind(extension: AnimeExtension.Available) { val nsfw = if (extension.isNsfw) "(18+)" else "" val lang = LanguageMapper.mapLanguageCodeToName(extension.lang) diff --git a/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt index 0ae7e01d..eed5bccc 100644 --- a/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt +++ b/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt @@ -1,5 +1,6 @@ package ani.dantotsu.settings.paging +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import android.view.animation.LinearInterpolator @@ -91,17 +92,13 @@ class MangaExtensionPagingSource( val availableExtensions = availableExtensionsFlow.filterNot { it.pkgName in installedExtensions } val query = searchQuery - val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true + val isNsfwEnabled: Boolean = loadData("NSFWExtension") ?: true val filteredExtensions = if (query.isEmpty()) { availableExtensions } else { availableExtensions.filter { it.name.contains(query, ignoreCase = true) } } - val filternfsw = if (isNsfwEnabled) { - filteredExtensions - } else { - filteredExtensions.filterNot { it.isNsfw } - } + val filternfsw = if (isNsfwEnabled) filteredExtensions else filteredExtensions.filterNot { it.isNsfw } return try { val sublist = filternfsw.subList( fromIndex = position, @@ -194,6 +191,7 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen } val extensionIconImageView: ImageView = binding.extensionIconImageView + @SuppressLint("SetTextI18n") fun bind(extension: MangaExtension.Available) { val nsfw = if (extension.isNsfw) "(18+)" else "" val lang = LanguageMapper.mapLanguageCodeToName(extension.lang) diff --git a/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt b/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt index 2029177b..8ee3f62d 100644 --- a/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt +++ b/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt @@ -42,14 +42,15 @@ class ThemeManager(private val context: Context) { .getString("theme", "PURPLE")!! val themeToApply = when (theme) { - "PURPLE" -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple "BLUE" -> if (useOLED) R.style.Theme_Dantotsu_BlueOLED else R.style.Theme_Dantotsu_Blue "GREEN" -> if (useOLED) R.style.Theme_Dantotsu_GreenOLED else R.style.Theme_Dantotsu_Green + "PURPLE" -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple "PINK" -> if (useOLED) R.style.Theme_Dantotsu_PinkOLED else R.style.Theme_Dantotsu_Pink + "SAIKOU" -> if (useOLED) R.style.Theme_Dantotsu_SaikouOLED else R.style.Theme_Dantotsu_Saikou "RED" -> if (useOLED) R.style.Theme_Dantotsu_RedOLED else R.style.Theme_Dantotsu_Red "LAVENDER" -> if (useOLED) R.style.Theme_Dantotsu_LavenderOLED else R.style.Theme_Dantotsu_Lavender + "EMERALD" -> if (useOLED) R.style.Theme_Dantotsu_EmeraldOLED else R.style.Theme_Dantotsu_Emerald "MONOCHROME (BETA)" -> if (useOLED) R.style.Theme_Dantotsu_MonochromeOLED else R.style.Theme_Dantotsu_Monochrome - "SAIKOU" -> if (useOLED) R.style.Theme_Dantotsu_SaikouOLED else R.style.Theme_Dantotsu_Saikou else -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple } @@ -109,14 +110,15 @@ class ThemeManager(private val context: Context) { companion object { enum class Theme(val theme: String) { - PURPLE("PURPLE"), BLUE("BLUE"), GREEN("GREEN"), + PURPLE("PURPLE"), PINK("PINK"), + SAIKOU("SAIKOU"), RED("RED"), LAVENDER("LAVENDER"), - MONOCHROME("MONOCHROME (BETA)"), - SAIKOU("SAIKOU"); + EMERALD("EMERALD"), + MONOCHROME("MONOCHROME (BETA)"); companion object { fun fromString(value: String): Theme { diff --git a/app/src/main/res/layout/fragment_manga_offline.xml b/app/src/main/res/layout/fragment_manga_offline.xml index 995953ea..be95cc1a 100644 --- a/app/src/main/res/layout/fragment_manga_offline.xml +++ b/app/src/main/res/layout/fragment_manga_offline.xml @@ -80,7 +80,43 @@ + + + + + + + + - + + android:paddingStart="25dp" + android:paddingEnd="25dp" + android:gravity="center" + android:scrollbars="none" + android:visibility="gone"/> + + diff --git a/app/src/main/res/layout/item_media_large.xml b/app/src/main/res/layout/item_media_large.xml index 83fcffa8..91aa35c7 100644 --- a/app/src/main/res/layout/item_media_large.xml +++ b/app/src/main/res/layout/item_media_large.xml @@ -150,7 +150,43 @@ android:text="@string/eps" /> + + + + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 73a2114d..fbda0709 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -65,32 +65,32 @@ + + + + + + + + - - - - - - + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 34281556..06f9958b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -24,6 +24,14 @@ #CD201F #a3a2a2 #E8EDEDED + #93DB00 + #68AF86 + #0096AE + #000000 + #FFE1F5FE + #FF81D4FA + #FF039BE5 + #FF01579B #00658e @@ -88,6 +96,7 @@ #41484D #000000 + #426916 #426916 #FFFFFF @@ -150,6 +159,7 @@ #44483D #000000 + #7c4997 #7C4997 #FFFFFF @@ -181,37 +191,38 @@ #7C4997 #CEC3CE #000000 - #E7B3FF - #4A1765 - #62307D - #F6D9FF - #D3C0D8 - #382C3E - #504255 - #F0DCF4 - #F5B7B7 - #4C2526 - #663B3B - #FFDAD9 - #FFB4AB - #93000A - #690005 - #FFDAD6 - #1D1B1E - #E8E0E5 - #1D1B1E - #E8E0E5 - #4B444D - #CEC3CE - #978E98 - #1D1B1E - #E8E0E5 - #7C4997 - #000000 - #E7B3FF - #4B444D - #000000 + #E7B3FF + #4A1765 + #62307D + #F6D9FF + #D3C0D8 + #382C3E + #504255 + #F0DCF4 + #F5B7B7 + #4C2526 + #663B3B + #FFDAD9 + #FFB4AB + #93000A + #690005 + #FFDAD6 + #1D1B1E + #E8E0E5 + #1D1B1E + #E8E0E5 + #4B444D + #CEC3CE + #978E98 + #1D1B1E + #E8E0E5 + #7C4997 + #000000 + #E7B3FF + #4B444D + #000000 + #e800ac #B30084 #FFFFFF @@ -274,7 +285,8 @@ #504349 #000000 - #FF007F + + #FF007F #FF007F #EEEEEE @@ -330,7 +342,7 @@ #00FF00 #00FF00 #00FF00 - #1C1B20 + #1C1B1E #EEEEEE #1C1B20 #EEEEEE @@ -345,135 +357,195 @@ #00FF00 #00FF00 - #c9000b - #C0000A - #FFFFFF - #FFDAD5 - #410001 - #775652 - #FFFFFF - #FFDAD5 - #2C1512 - #705C2E - #FFFFFF - #FCDFA6 - #261A00 - #BA1A1A - #FFDAD6 - #FFFFFF - #410002 - #FFFBFF - #201A19 - #FFFBFF - #201A19 - #F5DDDA - #534341 - #857370 - #FBEEEC - #362F2E - #FFB4AA - #000000 - #C0000A - #D8C2BE - #000000 - #FFB4AA - #690003 - #930005 - #FFDAD5 - #E7BDB7 - #442926 - #5D3F3B - #FFDAD5 - #DFC38C - #3E2E04 - #574419 - #FCDFA6 - #FFB4AB - #93000A - #690005 - #FFDAD6 - #201A19 - #EDE0DE - #201A19 - #EDE0DE - #534341 - #D8C2BE - #A08C89 - #201A19 - #EDE0DE - #C0000A - #000000 - #FFB4AA - #534341 - #000000 + + #c9000b + #C0000A + #FFFFFF + #FFDAD5 + #410001 + #775652 + #FFFFFF + #FFDAD5 + #2C1512 + #705C2E + #FFFFFF + #FCDFA6 + #261A00 + #BA1A1A + #FFDAD6 + #FFFFFF + #410002 + #FFFBFF + #201A19 + #FFFBFF + #201A19 + #F5DDDA + #534341 + #857370 + #FBEEEC + #362F2E + #FFB4AA + #000000 + #C0000A + #D8C2BE + #000000 + #FFB4AA + #690003 + #930005 + #FFDAD5 + #E7BDB7 + #442926 + #5D3F3B + #FFDAD5 + #DFC38C + #3E2E04 + #574419 + #FCDFA6 + #FFB4AB + #93000A + #690005 + #FFDAD6 + #201A19 + #EDE0DE + #201A19 + #EDE0DE + #534341 + #D8C2BE + #A08C89 + #201A19 + #EDE0DE + #C0000A + #000000 + #FFB4AA + #534341 + #000000 + + + #6750A4 + #6750A4 + #FFFFFF + #E9DDFF + #22005D + #625B71 + #FFFFFF + #E8DEF8 + #1E192B + #7E5260 + #FFFFFF + #FFD9E3 + #31101D + #BA1A1A + #FFDAD6 + #FFFFFF + #410002 + #FFFBFF + #1C1B1E + #FFFBFF + #1C1B1E + #E7E0EB + #49454E + #7A757F + #F4EFF4 + #313033 + #CFBCFF + #000000 + #6750A4 + #CAC4CF + #000000 + #CFBCFF + #381E72 + #4F378A + #E9DDFF + #CBC2DB + #332D41 + #4A4458 + #E8DEF8 + #EFB8C8 + #4A2532 + #633B48 + #FFD9E3 + #FFB4AB + #93000A + #690005 + #FFDAD6 + #1C1B1E + #E6E1E6 + #1C1B1E + #E6E1E6 + #49454E + #CAC4CF + #948F99 + #1C1B1E + #E6E1E6 + #6750A4 + #000000 + #CFBCFF + #49454E + #000000 + + + #14AEA7 + #006A65 + #FFFFFF + #70F7EF + #00201E + #4A6361 + #FFFFFF + #CCE8E5 + #051F1E + #48607B + #FFFFFF + #D0E4FF + #001D34 + #BA1A1A + #FFDAD6 + #FFFFFF + #410002 + #FAFDFB + #191C1C + #FAFDFB + #191C1C + #DAE5E3 + #3F4948 + #6F7978 + #EFF1F0 + #2D3131 + #4FDAD2 + #000000 + #006A65 + #BEC9C7 + #000000 + #4FDAD2 + #003734 + #00504C + #70F7EF + #B0CCC9 + #1B3533 + #324B49 + #CCE8E5 + #B0C9E7 + #19324A + #314962 + #D0E4FF + #FFB4AB + #93000A + #690005 + #FFDAD6 + #191C1C + #E0E3E2 + #191C1C + #E0E3E2 + #3F4948 + #BEC9C7 + #889391 + #191C1C + #E0E3E2 + #006A65 + #000000 + #4FDAD2 + #3F4948 + #000000 + + - #6750A4 - #6750A4 - #FFFFFF - #E9DDFF - #22005D - #625B71 - #FFFFFF - #E8DEF8 - #1E192B - #7E5260 - #FFFFFF - #FFD9E3 - #31101D - #BA1A1A - #FFDAD6 - #FFFFFF - #410002 - #FFFBFF - #1C1B1E - #FFFBFF - #1C1B1E - #E7E0EB - #49454E - #7A757F - #F4EFF4 - #313033 - #CFBCFF - #000000 - #6750A4 - #CAC4CF - #000000 - #CFBCFF - #381E72 - #4F378A - #E9DDFF - #CBC2DB - #332D41 - #4A4458 - #E8DEF8 - #EFB8C8 - #4A2532 - #633B48 - #FFD9E3 - #FFB4AB - #93000A - #690005 - #FFDAD6 - #1C1B1E - #E6E1E6 - #1C1B1E - #E6E1E6 - #49454E - #CAC4CF - #948F99 - #1C1B1E - #E6E1E6 - #6750A4 - #000000 - #CFBCFF - #49454E - #000000 - #93DB00 - #68AF86 - #0096AE - #000000 - #FFE1F5FE - #FF81D4FA - #FF039BE5 - #FF01579B diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d672ba53..5b893737 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -198,93 +198,6 @@ @color/md_theme_light_4_inversePrimary - - - - - - + + + + + + + + - + + + @color/bg_opp - + + + \ No newline at end of file From 549d7f9db36933fb9570e8b28773b6ef7eb11962 Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:11:06 -0600 Subject: [PATCH 10/29] better info on no extensions installed --- app/src/main/java/ani/dantotsu/parsers/BaseParser.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt index 03562f68..ba6057eb 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt @@ -156,9 +156,9 @@ abstract class BaseParser { } fun checkIfVariablesAreEmpty() { - if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `hostUrl` for the Parser") - if (name.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `name` for the Parser") - if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `saveName` for the Parser") + if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions") + if (name.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions") + if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions") } open var showUserText = "" From a8711241a7797d9bc1f95e174935fc2a7c9460f4 Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:15:47 -0600 Subject: [PATCH 11/29] default cast setting to visible --- app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt | 2 +- app/src/main/res/layout/activity_player_settings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt index 1589d66d..3a373cc1 100644 --- a/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt +++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettings.kt @@ -45,6 +45,6 @@ data class PlayerSettings( var skipTime: Int = 85, //Other - var cast: Boolean = false, + var cast: Boolean = true, var pip: Boolean = true ) : Serializable diff --git a/app/src/main/res/layout/activity_player_settings.xml b/app/src/main/res/layout/activity_player_settings.xml index 3980f9d6..5a5da548 100644 --- a/app/src/main/res/layout/activity_player_settings.xml +++ b/app/src/main/res/layout/activity_player_settings.xml @@ -1210,7 +1210,7 @@ android:fontFamily="@font/poppins_family" android:paddingStart="32dp" android:paddingEnd="32dp" - android:text="@string/show_cast_button_info" + android:text="" android:textSize="14sp" /> From 42c3b42c054eb27865333b837c5d90940dd2c0b6 Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Wed, 27 Dec 2023 10:30:50 -0600 Subject: [PATCH 12/29] incognito does not require app restart --- .../java/ani/dantotsu/home/AnimeFragment.kt | 71 +++++++++++-------- .../ani/dantotsu/home/AnimePageAdapter.kt | 13 ++++ .../java/ani/dantotsu/home/HomeFragment.kt | 18 +++++ .../java/ani/dantotsu/home/MangaFragment.kt | 59 ++++++++------- .../ani/dantotsu/home/MangaPageAdapter.kt | 13 ++++ .../ani/dantotsu/settings/SettingsActivity.kt | 1 - app/src/main/res/layout/fragment_home.xml | 2 +- app/src/main/res/layout/item_anime_page.xml | 2 +- app/src/main/res/layout/item_manga_page.xml | 2 +- 9 files changed, 121 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt index 1a08934d..8ecbe0a1 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt @@ -2,6 +2,7 @@ package ani.dantotsu.home import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle @@ -26,6 +27,7 @@ import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistAnimeViewModel import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.getUserId +import ani.dantotsu.currContext import ani.dantotsu.databinding.FragmentAnimeBinding import ani.dantotsu.loadData import ani.dantotsu.media.MediaAdaptor @@ -47,16 +49,17 @@ import kotlin.math.min class AnimeFragment : Fragment() { private var _binding: FragmentAnimeBinding? = null private val binding get() = _binding!! + private lateinit var animePageAdapter: AnimePageAdapter private var uiSettings: UserInterfaceSettings = - loadData("ui_settings") ?: UserInterfaceSettings() + loadData("ui_settings") ?: UserInterfaceSettings() val model: AnilistAnimeViewModel by activityViewModels() override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { _binding = FragmentAnimeBinding.inflate(inflater, container, false) return binding.root @@ -78,11 +81,11 @@ class AnimeFragment : Fragment() { if (displayCutout != null) { if (displayCutout.boundingRects.size > 0) { height = max( - statusBarHeight, - min( - displayCutout.boundingRects[0].width(), - displayCutout.boundingRects[0].height() - ) + statusBarHeight, + min( + displayCutout.boundingRects[0].width(), + displayCutout.boundingRects[0].height() + ) ) } } @@ -95,18 +98,18 @@ class AnimeFragment : Fragment() { binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px) - val animePageAdapter = AnimePageAdapter() + animePageAdapter = AnimePageAdapter() var loading = true if (model.notSet) { model.notSet = false model.searchResults = SearchResults( - "ANIME", - isAdult = false, - onList = false, - results = mutableListOf(), - hasNextPage = true, - sort = Anilist.sortBy[1] + "ANIME", + isAdult = false, + onList = false, + results = mutableListOf(), + hasNextPage = true, + sort = Anilist.sortBy[1] ) } val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity()) @@ -174,7 +177,7 @@ class AnimeFragment : Fragment() { } binding.animePageRecyclerView.addOnScrollListener(object : - RecyclerView.OnScrollListener() { + RecyclerView.OnScrollListener() { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { if (!v.canScrollVertically(1)) { if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) { @@ -214,19 +217,19 @@ class AnimeFragment : Fragment() { model.getTrending().observe(viewLifecycleOwner) { if (it != null) { animePageAdapter.updateTrending( - MediaAdaptor( - if (uiSettings.smallView) 3 else 2, - it, - requireActivity(), - viewPager = animePageAdapter.trendingViewPager - ) + MediaAdaptor( + if (uiSettings.smallView) 3 else 2, + it, + requireActivity(), + viewPager = animePageAdapter.trendingViewPager + ) ) animePageAdapter.updateAvatar() } } } binding.animePageScrollTop.translationY = - -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() + -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() } } @@ -244,13 +247,13 @@ class AnimeFragment : Fragment() { animePageAdapter.onSeasonLongClick = { i -> val (season, year) = Anilist.currentSeasons[i] ContextCompat.startActivity( - requireContext(), - Intent(requireContext(), SearchActivity::class.java) - .putExtra("type", "ANIME") - .putExtra("season", season) - .putExtra("seasonYear", year.toString()) - .putExtra("search", true), - null + requireContext(), + Intent(requireContext(), SearchActivity::class.java) + .putExtra("type", "ANIME") + .putExtra("season", season) + .putExtra("seasonYear", year.toString()) + .putExtra("search", true), + null ) true } @@ -277,6 +280,12 @@ class AnimeFragment : Fragment() { override fun onResume() { if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true) + if (animePageAdapter.trendingViewPager != null) { + animePageAdapter.setIncognito() + binding.root.requestApplyInsets() + binding.root.requestLayout() + } + super.onResume() } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index 19b1002d..43c1bc3e 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -157,6 +157,19 @@ class AnimePageAdapter : RecyclerView.Adapter("ui_settings") ?: UserInterfaceSettings() + binding.incognitoTextView.visibility = View.VISIBLE + if (!uiSettings.immersiveMode) { + binding.root.fitsSystemWindows = true + //holy deprecation + binding.root.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + binding.root.requestApplyInsets() + binding.root.requestLayout() + } + } else { + binding.incognitoTextView.visibility = View.GONE + } super.onResume() } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt index 14870b7f..55ac8b2f 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt @@ -2,6 +2,7 @@ package ani.dantotsu.home import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.content.Context import android.os.Build import android.os.Bundle import android.view.LayoutInflater @@ -24,6 +25,7 @@ import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistMangaViewModel import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.getUserId +import ani.dantotsu.currContext import ani.dantotsu.databinding.FragmentMangaBinding import ani.dantotsu.loadData import ani.dantotsu.media.MediaAdaptor @@ -43,16 +45,17 @@ import kotlin.math.min class MangaFragment : Fragment() { private var _binding: FragmentMangaBinding? = null private val binding get() = _binding!! + private lateinit var mangaPageAdapter: MangaPageAdapter private var uiSettings: UserInterfaceSettings = - loadData("ui_settings") ?: UserInterfaceSettings() + loadData("ui_settings") ?: UserInterfaceSettings() val model: AnilistMangaViewModel by activityViewModels() override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { _binding = FragmentMangaBinding.inflate(inflater, container, false) return binding.root @@ -73,11 +76,11 @@ class MangaFragment : Fragment() { if (displayCutout != null) { if (displayCutout.boundingRects.size > 0) { height = max( - statusBarHeight, - min( - displayCutout.boundingRects[0].width(), - displayCutout.boundingRects[0].height() - ) + statusBarHeight, + min( + displayCutout.boundingRects[0].width(), + displayCutout.boundingRects[0].height() + ) ) } } @@ -90,23 +93,23 @@ class MangaFragment : Fragment() { binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px) - val mangaPageAdapter = MangaPageAdapter() + mangaPageAdapter = MangaPageAdapter() var loading = true if (model.notSet) { model.notSet = false model.searchResults = SearchResults( - "MANGA", - isAdult = false, - onList = false, - results = arrayListOf(), - hasNextPage = true, - sort = Anilist.sortBy[1] + "MANGA", + isAdult = false, + onList = false, + results = arrayListOf(), + hasNextPage = true, + sort = Anilist.sortBy[1] ) } val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity()) val progressAdaptor = ProgressAdapter(searched = model.searched) binding.mangaPageRecyclerView.adapter = - ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor) + ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor) val layout = LinearLayoutManager(requireContext()) binding.mangaPageRecyclerView.layoutManager = layout @@ -132,7 +135,7 @@ class MangaFragment : Fragment() { } binding.mangaPageRecyclerView.addOnScrollListener(object : - RecyclerView.OnScrollListener() { + RecyclerView.OnScrollListener() { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { if (!v.canScrollVertically(1)) { if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) { @@ -172,19 +175,19 @@ class MangaFragment : Fragment() { model.getTrending().observe(viewLifecycleOwner) { if (it != null) { mangaPageAdapter.updateTrending( - MediaAdaptor( - if (uiSettings.smallView) 3 else 2, - it, - requireActivity(), - viewPager = mangaPageAdapter.trendingViewPager - ) + MediaAdaptor( + if (uiSettings.smallView) 3 else 2, + it, + requireActivity(), + viewPager = mangaPageAdapter.trendingViewPager + ) ) mangaPageAdapter.updateAvatar() } } } binding.mangaPageScrollTop.translationY = - -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() + -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() } } @@ -251,6 +254,12 @@ class MangaFragment : Fragment() { override fun onResume() { if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true) + //make sure mangaPageAdapter is initialized + if (mangaPageAdapter.trendingViewPager != null) { + mangaPageAdapter.setIncognito() + binding.root.requestApplyInsets() + binding.root.requestLayout() + } super.onResume() } diff --git a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt index dc597a50..058dd3ca 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt @@ -150,6 +150,19 @@ class MangaPageAdapter : RecyclerView.Adapter getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() .putBoolean("incognito", isChecked).apply() - restartApp() } var previousStart: View = when (uiSettings.defaultStartUpTab) { diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 4b738d12..12d03586 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -24,7 +24,7 @@ android:text="Incognito Mode" android:fontFamily="@font/poppins_bold" android:textSize="15sp" - android:textColor="#4f2dbd" + android:textColor="?attr/colorPrimary" android:layout_gravity="bottom|center_horizontal" android:padding="8dp" android:visibility="gone"/> diff --git a/app/src/main/res/layout/item_anime_page.xml b/app/src/main/res/layout/item_anime_page.xml index ef29558a..0657b349 100644 --- a/app/src/main/res/layout/item_anime_page.xml +++ b/app/src/main/res/layout/item_anime_page.xml @@ -16,7 +16,7 @@ android:text="Incognito Mode" android:fontFamily="@font/poppins_bold" android:textSize="15sp" - android:textColor="#4f2dbd" + android:textColor="?attr/colorPrimary" android:layout_gravity="bottom|center_horizontal" android:background="#00FFFFFF" android:padding="8dp" diff --git a/app/src/main/res/layout/item_manga_page.xml b/app/src/main/res/layout/item_manga_page.xml index c88c38f5..cda24cc5 100644 --- a/app/src/main/res/layout/item_manga_page.xml +++ b/app/src/main/res/layout/item_manga_page.xml @@ -16,7 +16,7 @@ android:text="Incognito Mode" android:fontFamily="@font/poppins_bold" android:textSize="15sp" - android:textColor="#4f2dbd" + android:textColor="?attr/colorPrimary" android:layout_gravity="bottom|center_horizontal" android:background="#00FFFFFF" android:padding="8dp" From 7684a15e9492ba1fd928c9707783570504d6864f Mon Sep 17 00:00:00 2001 From: Sadwhy <99601717+Sadwhy@users.noreply.github.com> Date: Thu, 28 Dec 2023 16:13:42 +0600 Subject: [PATCH 13/29] Changed a few strings (#101) * :) Changed some strings * More strings * New string --- .github/workflows/beta.yml | 2 +- app/src/main/res/values-en-rDW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index a9145485..1f24338c 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -50,7 +50,7 @@ jobs: shell: bash run: | contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" ) - curl -F "payload_json={\"content\":\" everyone **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }} + curl -F "payload_json={\"content\":\" Debug-Build **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }} - name: Delete Old Pre-Releases id: delete-pre-releases diff --git a/app/src/main/res/values-en-rDW/strings.xml b/app/src/main/res/values-en-rDW/strings.xml index e50ccd3a..293ce3fc 100644 --- a/app/src/main/res/values-en-rDW/strings.xml +++ b/app/src/main/res/values-en-rDW/strings.xml @@ -228,7 +228,7 @@ Subtitle Background Color Subtitle Window Color "The subtitle window is the part left and right from them. (where the background isn\'t)" - Note: Changing Subtitle formatting only works with Soft-Subbed Sources such as Zoro! + Note: Changing above settings only affects Soft-Subtitles! Subtitle Font Subtitle Size diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a8da5ba..23e646c5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -227,7 +227,7 @@ Subtitle Background Color Subtitle Window Color "The subtitle window is the part left and right from them. (where the background isn\'t)" - Note: Changing Subtitle formatting only works with Soft-Subbed Sources such as Zoro! + Note: Changing above settings only affects Soft-Subtitles! Subtitle Font Subtitle Size From bbc986784b69540e85bc99908988443487b3ca60 Mon Sep 17 00:00:00 2001 From: aayush262 <99584765+aayush2622@users.noreply.github.com> Date: Thu, 28 Dec 2023 15:45:10 +0530 Subject: [PATCH 14/29] small changes (#102) --- .../download/manga/OfflineMangaFragment.kt | 10 +++++++++ .../ani/dantotsu/home/AnimePageAdapter.kt | 6 ----- .../java/ani/dantotsu/home/HomeFragment.kt | 8 ------- .../ani/dantotsu/home/MangaPageAdapter.kt | 6 ----- .../ani/dantotsu/media/user/ListActivity.kt | 5 ++--- .../settings/UserInterfaceSettings.kt | 1 - .../settings/UserInterfaceSettingsActivity.kt | 7 ------ .../main/res/layout/activity_extensions.xml | 17 -------------- .../activity_user_interface_settings.xml | 22 ------------------- 9 files changed, 12 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt index 85a06bed..d405583c 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -14,6 +14,8 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import android.view.animation.LayoutAnimationController import android.view.animation.OvershootInterpolator import android.widget.AbsListView import android.widget.AutoCompleteTextView @@ -145,6 +147,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { gridView.visibility = View.GONE gridView = view.findViewById(R.id.gridView1) gridView.adapter = adapter + gridView.scheduleLayoutAnimation() gridView.visibility = View.VISIBLE adapter.notifyNewGrid() } @@ -152,8 +155,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { gridView = if(style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1) gridView.visibility = View.VISIBLE getDownloads() + + val fadeIn = AlphaAnimation(0f, 1f) + fadeIn.duration = 200 // animations pog + val animation = LayoutAnimationController(fadeIn) + + gridView.layoutAnimation = animation adapter = OfflineMangaAdapter(requireContext(), downloads, this) gridView.adapter = adapter + gridView.scheduleLayoutAnimation() gridView.setOnItemClickListener { parent, view, position, id -> // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index 43c1bc3e..732ac43f 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -77,12 +77,6 @@ class AnimePageAdapter : RecyclerView.Adapter("ui_settings") ?: UserInterfaceSettings() - if (!uiSettings.immersiveModeList) { + if (!uiSettings.immersiveMode) { this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv) binding.root.fitsSystemWindows = true @@ -78,8 +78,7 @@ class ListActivity : AppCompatActivity() { setContentView(binding.root) val anime = intent.getBooleanExtra("anime", true) - binding.listTitle.text = - intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List" + binding.listTitle.text = (if (anime) "Anime" else "Manga") + " List" binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { diff --git a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettings.kt b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettings.kt index 15fd20a8..39dd18b1 100644 --- a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettings.kt +++ b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettings.kt @@ -10,7 +10,6 @@ data class UserInterfaceSettings( //App var immersiveMode: Boolean = false, - var immersiveModeList: Boolean = false, var smallView: Boolean = true, var defaultStartUpTab: Int = 1, var homeLayoutShow: MutableList = mutableListOf( diff --git a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt index 189d691f..fb49c496 100644 --- a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt @@ -68,13 +68,6 @@ class UserInterfaceSettingsActivity : AppCompatActivity() { saveData(ui, settings) restartApp() } - binding.uiSettingsImmersiveList.isChecked = settings.immersiveModeList - binding.uiSettingsImmersiveList.setOnCheckedChangeListener { _, isChecked -> - settings.immersiveModeList = isChecked - saveData(ui, settings) - restartApp() - } - binding.uiSettingsBannerAnimation.isChecked = settings.bannerAnimations binding.uiSettingsBannerAnimation.setOnCheckedChangeListener { _, isChecked -> settings.bannerAnimations = isChecked diff --git a/app/src/main/res/layout/activity_extensions.xml b/app/src/main/res/layout/activity_extensions.xml index 85a8db87..728236ba 100644 --- a/app/src/main/res/layout/activity_extensions.xml +++ b/app/src/main/res/layout/activity_extensions.xml @@ -81,23 +81,6 @@ app:tabPaddingStart="16dp" app:tabTextAppearance="@style/NavBarText" app:tabGravity="fill"> - - - - - - - Date: Thu, 28 Dec 2023 06:38:45 -0600 Subject: [PATCH 15/29] rough outline for downloading anime --- app/src/main/AndroidManifest.xml | 8 +- .../main/java/ani/dantotsu/MainActivity.kt | 22 +- .../aniyomi/anime/custom/InjektModules.kt | 8 +- .../download/anime/AnimeDownloaderService.kt | 420 ++++++++++++++++++ .../ani/dantotsu/download/video/Helper.kt | 104 ++++- .../download/video/MyDownloadService.kt | 2 +- .../media/anime/AnimeWatchFragment.kt | 8 + .../java/ani/dantotsu/media/anime/Episode.kt | 2 +- .../media/anime/SelectorDialogFragment.kt | 40 +- .../ani/dantotsu/parsers/AniyomiAdapter.kt | 54 ++- app/src/main/res/values/strings.xml | 1 + 11 files changed, 643 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7750a958..23f4b20d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -274,8 +274,9 @@ android:exported="true" /> - + android:exported="false" + android:foregroundServiceType="dataSync"> + @@ -297,6 +298,9 @@ android:name=".download.novel.NovelDownloaderService" android:exported="false" android:foregroundServiceType="dataSync" /> + () + + private val downloadJobs = mutableMapOf() + private val mutex = Mutex() + private var isCurrentlyProcessing = false + + override fun onBind(intent: Intent?): IBinder? { + // This is only required for bound services. + return null + } + + override fun onCreate() { + super.onCreate() + notificationManager = NotificationManagerCompat.from(this) + builder = NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply { + setContentTitle("Anime Download Progress") + setSmallIcon(R.drawable.ic_round_download_24) + priority = NotificationCompat.PRIORITY_DEFAULT + setOnlyAlertOnce(true) + setProgress(0, 0, false) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + NOTIFICATION_ID, + builder.build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { + startForeground(NOTIFICATION_ID, builder.build()) + } + ContextCompat.registerReceiver( + this, + cancelReceiver, + IntentFilter(ACTION_CANCEL_DOWNLOAD), + ContextCompat.RECEIVER_EXPORTED + ) + } + + override fun onDestroy() { + super.onDestroy() + AnimeServiceDataSingleton.downloadQueue.clear() + downloadJobs.clear() + AnimeServiceDataSingleton.isServiceRunning = false + unregisterReceiver(cancelReceiver) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + snackString("Download started") + val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + serviceScope.launch { + mutex.withLock { + if (!isCurrentlyProcessing) { + isCurrentlyProcessing = true + processQueue() + isCurrentlyProcessing = false + } + } + } + return START_NOT_STICKY + } + + private fun processQueue() { + CoroutineScope(Dispatchers.Default).launch { + while (AnimeServiceDataSingleton.downloadQueue.isNotEmpty()) { + val task = AnimeServiceDataSingleton.downloadQueue.poll() + if (task != null) { + val job = launch { download(task) } + mutex.withLock { + downloadJobs[task.getTaskName()] = job + } + job.join() // Wait for the job to complete before continuing to the next task + mutex.withLock { + downloadJobs.remove(task.getTaskName()) + } + updateNotification() // Update the notification after each task is completed + } + if (AnimeServiceDataSingleton.downloadQueue.isEmpty()) { + withContext(Dispatchers.Main) { + stopSelf() // Stop the service when the queue is empty + } + } + } + } + } + + @UnstableApi + fun cancelDownload(taskName: String) { + CoroutineScope(Dispatchers.Default).launch { + mutex.withLock { + val url = AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url ?: "" + DownloadService.sendRemoveDownload( + this@AnimeDownloaderService, + MyDownloadService::class.java, + url, + false + ) + downloadJobs[taskName]?.cancel() + downloadJobs.remove(taskName) + AnimeServiceDataSingleton.downloadQueue.removeAll { it.getTaskName() == taskName } + updateNotification() // Update the notification after cancellation + } + } + } + + private fun updateNotification() { + // Update the notification to reflect the current state of the queue + val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size + val text = if (pendingDownloads > 0) { + "Pending downloads: $pendingDownloads" + } else { + "All downloads completed" + } + builder.setContentText(text) + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + + @androidx.annotation.OptIn(UnstableApi::class) suspend fun download(task: DownloadTask) { + try { + val downloadManager = Helper.downloadManager(this@AnimeDownloaderService) + withContext(Dispatchers.Main) { + val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + this@AnimeDownloaderService, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } + + builder.setContentText("Downloading ${task.title} - ${task.episode}") + if (notifi) { + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + + broadcastDownloadStarted(task.getTaskName()) + + currActivity()?.let { + Helper.downloadVideo( + it, + task.video, + task.subtitle) + } + + saveMediaInfo(task) + downloadsManager.addDownload( + Download( + task.title, + task.episode, + Download.Type.ANIME, + ) + ) + + // periodically check if the download is complete + while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) { + val download = downloadManager.downloadIndex.getDownload(task.video.file.url) + if (download != null) { + if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) { + logger("Download failed") + builder.setContentText("${task.title} - ${task.episode} Download failed") + .setProgress(0, 0, false) + notificationManager.notify(NOTIFICATION_ID, builder.build()) + snackString("${task.title} - ${task.episode} Download failed") + broadcastDownloadFailed(task.getTaskName()) + break + } + if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) { + logger("Download completed") + builder.setContentText("${task.title} - ${task.episode} Download completed") + .setProgress(0, 0, false) + notificationManager.notify(NOTIFICATION_ID, builder.build()) + snackString("${task.title} - ${task.episode} Download completed") + getSharedPreferences(getString(R.string.anime_downloads), Context.MODE_PRIVATE).edit().putString( + task.getTaskName(), + task.video.file.url + ).apply() + broadcastDownloadFinished(task.getTaskName()) + break + } + if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) { + logger("Download stopped") + builder.setContentText("${task.title} - ${task.episode} Download stopped") + .setProgress(0, 0, false) + notificationManager.notify(NOTIFICATION_ID, builder.build()) + snackString("${task.title} - ${task.episode} Download stopped") + break + } + broadcastDownloadProgress(task.getTaskName(), download.percentDownloaded.toInt()) + builder.setProgress(100, download.percentDownloaded.toInt(), false) + if (notifi) { + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + } + kotlinx.coroutines.delay(2000) + } + } + } catch (e: Exception) { + logger("Exception while downloading file: ${e.message}") + snackString("Exception while downloading file: ${e.message}") + FirebaseCrashlytics.getInstance().recordException(e) + broadcastDownloadFailed(task.getTaskName()) + } + } + + @OptIn(DelicateCoroutinesApi::class) + private fun saveMediaInfo(task: DownloadTask) { + GlobalScope.launch(Dispatchers.IO) { + val directory = File( + getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Anime/${task.title}" + ) + if (!directory.exists()) directory.mkdirs() + + val file = File(directory, "media.json") + val gson = GsonBuilder() + .registerTypeAdapter(SChapter::class.java, InstanceCreator { + SChapterImpl() // Provide an instance of SChapterImpl + }) + .registerTypeAdapter(SAnime::class.java, InstanceCreator { + SAnimeImpl() // Provide an instance of SAnimeImpl + }) + .registerTypeAdapter(SEpisode::class.java, InstanceCreator { + SEpisodeImpl() // Provide an instance of SEpisodeImpl + }) + .create() + val mediaJson = gson.toJson(task.sourceMedia) + val media = gson.fromJson(mediaJson, Media::class.java) + if (media != null) { + media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") } + media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") } + + val jsonString = gson.toJson(media) + withContext(Dispatchers.Main) { + file.writeText(jsonString) + } + } + } + } + + + private suspend fun downloadImage(url: String, directory: File, name: String): String? = + withContext(Dispatchers.IO) { + var connection: HttpURLConnection? = null + println("Downloading url $url") + try { + connection = URL(url).openConnection() as HttpURLConnection + connection.connect() + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}") + } + + val file = File(directory, name) + FileOutputStream(file).use { output -> + connection.inputStream.use { input -> + input.copyTo(output) + } + } + return@withContext file.absolutePath + } catch (e: Exception) { + e.printStackTrace() + withContext(Dispatchers.Main) { + Toast.makeText( + this@AnimeDownloaderService, + "Exception while saving ${name}: ${e.message}", + Toast.LENGTH_LONG + ).show() + } + null + } finally { + connection?.disconnect() + } + } + + private fun broadcastDownloadStarted(chapterNumber: String) { + val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply { + putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, chapterNumber) + } + sendBroadcast(intent) + } + + private fun broadcastDownloadFinished(chapterNumber: String) { + val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FINISHED).apply { + putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, chapterNumber) + } + sendBroadcast(intent) + } + + private fun broadcastDownloadFailed(chapterNumber: String) { + val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FAILED).apply { + putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, chapterNumber) + } + sendBroadcast(intent) + } + + private fun broadcastDownloadProgress(chapterNumber: String, progress: Int) { + val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_PROGRESS).apply { + putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, chapterNumber) + putExtra("progress", progress) + } + sendBroadcast(intent) + } + + private val cancelReceiver = object : BroadcastReceiver() { + @androidx.annotation.OptIn(UnstableApi::class) override fun onReceive(context: Context, intent: Intent) { + if (intent.action == ACTION_CANCEL_DOWNLOAD) { + val taskName = intent.getStringExtra(EXTRA_TASK_NAME) + taskName?.let { + cancelDownload(it) + } + } + } + } + + + data class DownloadTask( + val title: String, + val episode: String, + val video: Video, + val subtitle: Subtitle? = null, + val sourceMedia: Media? = null, + val retries: Int = 2, + val simultaneousDownloads: Int = 2, + ) { + fun getTaskName(): String { + return "$title - $episode" + } + } + + companion object { + private const val NOTIFICATION_ID = 1103 + const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download" + const val EXTRA_TASK_NAME = "extra_task_name" + } +} + +object AnimeServiceDataSingleton { + var video: Video? = null + var sourceMedia: Media? = null + var downloadQueue: Queue = ConcurrentLinkedQueue() + + @Volatile + var isServiceRunning: Boolean = false +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt index 2dcf31c6..056e96ee 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -1,8 +1,17 @@ package ani.dantotsu.download.video +import android.Manifest import android.annotation.SuppressLint +import android.app.Activity import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build +import android.util.Log +import androidx.annotation.OptIn +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes @@ -15,6 +24,7 @@ import androidx.media3.datasource.cache.NoOpCacheEvictor import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.DownloadHelper import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadService @@ -22,7 +32,10 @@ import androidx.media3.exoplayer.scheduler.Requirements import androidx.media3.ui.TrackSelectionDialogBuilder import ani.dantotsu.R import ani.dantotsu.defaultHeaders +import ani.dantotsu.download.anime.AnimeDownloaderService +import ani.dantotsu.download.anime.AnimeServiceDataSingleton import ani.dantotsu.logError +import ani.dantotsu.media.Media import ani.dantotsu.okHttpClient import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.SubtitleType @@ -37,6 +50,7 @@ import java.util.concurrent.* object Helper { + var simpleCache: SimpleCache? = null @SuppressLint("UnsafeOptInUsageError") fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) { val dataSourceFactory = DataSource.Factory { @@ -114,13 +128,13 @@ object Helper { private var download: DownloadManager? = null - private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads" + private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads" @Synchronized @UnstableApi fun downloadManager(context: Context): DownloadManager { return download ?: let { - val database = StandaloneDatabaseProvider(context) + val database = Injekt.get() val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) val dataSourceFactory = DataSource.Factory { //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() @@ -133,17 +147,42 @@ object Helper { } dataSource } - DownloadManager( + val threadPoolSize = Runtime.getRuntime().availableProcessors() + val executorService = Executors.newFixedThreadPool(threadPoolSize) + val downloadManager = DownloadManager( context, database, - SimpleCache(downloadDirectory, NoOpCacheEvictor(), database), + getSimpleCache(context), dataSourceFactory, - Executor(Runnable::run) + executorService ).apply { requirements = Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) maxParallelDownloads = 3 } + downloadManager.addListener( + object : DownloadManager.Listener { // Override methods of interest here. + override fun onDownloadChanged( + downloadManager: DownloadManager, + download: Download, + finalException: Exception? + ) { + if (download.state == Download.STATE_COMPLETED) { + Log.e("Downloader", "Download Completed") + } else if (download.state == Download.STATE_FAILED) { + Log.e("Downloader", "Download Failed") + } else if (download.state == Download.STATE_STOPPED) { + Log.e("Downloader", "Download Stopped") + } else if (download.state == Download.STATE_QUEUED) { + Log.e("Downloader", "Download Queued") + } else if (download.state == Download.STATE_DOWNLOADING) { + Log.e("Downloader", "Download Downloading") + } + } + } + ) + + downloadManager } } @@ -159,4 +198,59 @@ object Helper { } return downloadDirectory!! } + + fun startAnimeDownloadService( + context: Context, + title: String, + episode: String, + video: Video, + subtitle: Subtitle? = null, + sourceMedia: Media? = null + ) { + if (!isNotificationPermissionGranted(context)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ActivityCompat.requestPermissions( + context as Activity, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + 1 + ) + } + } + + val downloadTask = AnimeDownloaderService.DownloadTask( + title, + episode, + video, + subtitle, + sourceMedia + ) + AnimeServiceDataSingleton.downloadQueue.offer(downloadTask) + + if (!AnimeServiceDataSingleton.isServiceRunning) { + val intent = Intent(context, AnimeDownloaderService::class.java) + ContextCompat.startForegroundService(context, intent) + AnimeServiceDataSingleton.isServiceRunning = true + } + } + + @OptIn(UnstableApi::class) private fun getSimpleCache(context: Context): SimpleCache { + return if (simpleCache == null) { + val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) + val database = Injekt.get() + simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database) + simpleCache!! + } else { + simpleCache!! + } + } + + private fun isNotificationPermissionGranted(context: Context): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } + return true + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt b/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt index 3a26ac1d..8c0d56d2 100644 --- a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt +++ b/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt @@ -11,7 +11,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler import ani.dantotsu.R @UnstableApi -class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) { +class MyDownloadService : DownloadService(1, 2000, "download_service", R.string.downloads, 0) { companion object { private const val JOB_ID = 1 private const val FOREGROUND_NOTIFICATION_ID = 1 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 df352220..5dba0e85 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -424,4 +424,12 @@ class AnimeWatchFragment : Fragment() { state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() } + companion object { + const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED" + const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED" + const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED" + const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS" + const val EXTRA_EPISODE_NUMBER = "extra_episode_number" + } + } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/anime/Episode.kt b/app/src/main/java/ani/dantotsu/media/anime/Episode.kt index 14615246..cc4ce613 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/Episode.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/Episode.kt @@ -14,7 +14,7 @@ data class Episode( var selectedExtractor: String? = null, var selectedVideo: Int = 0, var selectedSubtitle: Int? = -1, - var extractors: MutableList? = null, + @Transient var extractors: MutableList? = null, @Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null, var allStreams: Boolean = false, var watched: Long? = null, diff --git a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt index d42bcd94..c9d734f4 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -20,6 +20,7 @@ import ani.dantotsu.* import ani.dantotsu.databinding.BottomSheetSelectorBinding import ani.dantotsu.databinding.ItemStreamBinding import ani.dantotsu.databinding.ItemUrlBinding +import ani.dantotsu.download.video.Helper import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.others.Download.download @@ -214,7 +215,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { val extractor = links[position] - holder.binding.streamName.text = extractor.server.name + holder.binding.streamName.text = ""//extractor.server.name + holder.binding.streamName.visibility = View.GONE holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext()) holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor) @@ -256,10 +258,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { override fun onBindViewHolder(holder: UrlViewHolder, position: Int) { val binding = holder.binding val video = extractor.videos[position] - binding.urlQuality.text = - if (video.quality != null) "${video.quality}p" else "Default Quality" - binding.urlNote.text = video.extraNote ?: "" - binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE + //binding.urlQuality.text = + // if (video.quality != null) "${video.quality}p" else "Default Quality" + //binding.urlNote.text = video.extraNote ?: "" + //binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE binding.urlDownload.visibility = View.VISIBLE binding.urlDownload.setSafeOnClickListener { media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = @@ -267,11 +269,23 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = position binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - download( - requireActivity(), - media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!, - media!!.userPreferredName - ) + //download( + // requireActivity(), + // media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!, + // media!!.userPreferredName + //) + val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!! + val video = if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null + if (video != null) { + Helper.startAnimeDownloadService( + requireActivity(), + media!!.userPreferredName, + episode.number, + video, + null, + media + ) + } dismiss() } if (video.format == VideoType.CONTAINER) { @@ -282,11 +296,13 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { "#.##" ).format(video.size ?: 0).toString() + " MB")) } else { - binding.urlQuality.text = "Multi Quality" if ((loadData("settings_download_manager") ?: 0) == 0) { - binding.urlDownload.visibility = View.GONE + ////binding.urlDownload.visibility = View.GONE } } + binding.urlNote.visibility = View.VISIBLE + binding.urlNote.text = video.format.name + binding.urlQuality.text = extractor.server.name } override fun getItemCount(): Int = extractor.videos.size diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 89a6087c..27aece5f 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension +import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page @@ -41,6 +42,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import okhttp3.Request import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File @@ -112,7 +114,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { seasonGroups.keys.sorted().flatMap { season -> seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode -> if (episode.episode_number != 0f) { // Skip renumbering for episode number 0 - val potentialNumber = AnimeNameAdapter.findEpisodeNumber(episode.name) + val potentialNumber = + AnimeNameAdapter.findEpisodeNumber(episode.name) if (potentialNumber != null) { episode.episode_number = potentialNumber } else { @@ -613,6 +616,10 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { val fileName = queryPairs.find { it.first == "file" }?.second ?: "" format = getVideoType(fileName) + if (format == null) { + val networkHelper = Injekt.get() + format = headRequest(videoUrl, networkHelper) + } } // If the format is still undetermined, log an error or handle it appropriately @@ -630,12 +637,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { number, format, FileUrl(videoUrl, headersMap), - aniVideo.totalContentLength.toDouble() + if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble() ) } private fun getVideoType(fileName: String): VideoType? { - return when { + val type = when { fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith( ".mkv", ignoreCase = true @@ -645,6 +652,47 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH else -> null } + + return type + } + + private fun headRequest(fileName: String, networkHelper: NetworkHelper): VideoType? { + return try { + logger("attempting head request for $fileName") + val request = Request.Builder() + .url(fileName) + .head() + .build() + + networkHelper.client.newCall(request).execute().use { response -> + val contentType = response.header("Content-Type") + val contentDisposition = response.header("Content-Disposition") + + if (contentType != null) { + when { + contentType.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8 + contentType.contains("dash", ignoreCase = true) -> VideoType.DASH + contentType.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER + else -> null + } + } else if (contentDisposition != null) { + when { + contentDisposition.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8 + contentDisposition.contains("dash", ignoreCase = true) -> VideoType.DASH + contentDisposition.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER + else -> null + } + } else { + logger("failed head request for $fileName") + null + } + + } + } catch (e: Exception) { + logger("Exception in headRequest: $e") + null + } + } private fun TrackToSubtitle(track: Track): Subtitle { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2668a0ab..6a8da5ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -645,5 +645,6 @@ Add widget This is an app widget description Airing Image + animeDownloads From 62b1a3b900b5f73df514e4683d0a137ce75e4c63 Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Thu, 28 Dec 2023 19:27:03 -0600 Subject: [PATCH 16/29] move countdown to 28 days --- app/src/main/java/ani/dantotsu/Functions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt index 6a789743..2ac075f0 100644 --- a/app/src/main/java/ani/dantotsu/Functions.kt +++ b/app/src/main/java/ani/dantotsu/Functions.kt @@ -673,7 +673,7 @@ fun copyToClipboard(string: String, toast: Boolean = true) { @SuppressLint("SetTextI18n") fun countDown(media: Media, view: ViewGroup) { - if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) { + if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) { val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false) view.addView(v.root, 0) v.mediaCountdownText.text = From 41830dba4d59c9e5c03deeb0bc6aaf79be6f20aa Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Thu, 28 Dec 2023 21:57:20 -0600 Subject: [PATCH 17/29] tap between manga pages (paged) --- .../manga/mangareader/BaseImageAdapter.kt | 5 +- .../media/manga/mangareader/ImageAdapter.kt | 10 +++ .../manga/mangareader/MangaReaderActivity.kt | 64 +++++++++++++++++-- app/src/main/res/layout/item_image.xml | 2 +- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt index 6a18b9e8..d1732b5a 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt @@ -89,7 +89,7 @@ abstract class BaseImageAdapter( } } else { val detector = GestureDetectorCompat(view.context, object : GesturesListener() { - override fun onSingleClick(event: MotionEvent) = activity.handleController() + override fun onSingleClick(event: MotionEvent) = activity.handleController(event = event) }) view.findViewById(R.id.imgProgCover).apply { setOnTouchListener { _, event -> @@ -112,6 +112,9 @@ abstract class BaseImageAdapter( activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) } } + abstract fun isZoomed(): Boolean + abstract fun setZoom(zoom: Float) + abstract suspend fun loadImage(position: Int, parent: View): Boolean companion object { diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt index 97651499..45ae40f0 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt @@ -91,4 +91,14 @@ open class ImageAdapter( } override fun getItemCount(): Int = images.size + + override fun isZoomed(): Boolean { + val imageView = activity.findViewById(R.id.imgProgImageNoGestures) + return imageView.scale > imageView.minScale + } + + override fun setZoom(zoom: Float) { + val imageView = activity.findViewById(R.id.imgProgImageNoGestures) + imageView.setScaleAndCenter(zoom, imageView.center) + } } diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt index d6d72871..067ee56f 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt @@ -6,6 +6,7 @@ import android.app.AlertDialog import android.content.Context import android.content.Intent import android.content.res.Configuration +import android.content.res.Resources import android.graphics.Bitmap import android.os.Build import android.os.Bundle @@ -702,8 +703,60 @@ class MangaReaderActivity : AppCompatActivity() { goneTimer.schedule(timerTask, controllerDuration) } - fun handleController(shouldShow: Boolean? = null) { + enum class pressPos { + LEFT, RIGHT, CENTER + } + + fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) { + var pressLocation = pressPos.CENTER if (!sliding) { + if (event != null && settings.default.layout == PAGED) { + if (event.action != MotionEvent.ACTION_UP) return + val x = event.rawX.toInt() + val y = event.rawY.toInt() + val screenWidth = Resources.getSystem().displayMetrics.widthPixels + //if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left + if (screenWidth / 5 in (x + 1).. screenWidth - screenWidth / 5 && y > screenWidth / 5) { + pressLocation = if (settings.default.direction == RIGHT_TO_LEFT) { + pressPos.LEFT + } else { + pressPos.RIGHT + } + } + } + + // if pressLocation is left or right go to previous or next page (paged mode only) + if (pressLocation == pressPos.LEFT) { + + if (binding.mangaReaderPager.currentItem > 0) { + //if the current images zoomed in, go back to normal before going to previous page + if (imageAdapter?.isZoomed() == true) { + imageAdapter?.setZoom(1f) + } + binding.mangaReaderPager.currentItem -= 1 + return + } + + } else if (pressLocation == pressPos.RIGHT) { + if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) { + //if the current images zoomed in, go back to normal before going to next page + if (imageAdapter?.isZoomed() == true) { + imageAdapter?.setZoom(1f) + } + //if right to left, go to previous page + binding.mangaReaderPager.currentItem += 1 + return + } + } + if (!settings.showSystemBars) { hideBars() checkNotch() @@ -796,7 +849,7 @@ class MangaReaderActivity : AppCompatActivity() { private fun progress(runnable: Runnable) { if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) { - if (showProgressDialog) { + if (showProgressDialog) { val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null) val checkbox = dialogView.findViewById(R.id.dialog_checkbox) @@ -805,8 +858,9 @@ class MangaReaderActivity : AppCompatActivity() { saveData("${media.id}_progressDialog", isChecked) showProgressDialog = !isChecked } - val incognito = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) - ?.getBoolean("incognito", false) ?: false + val incognito = + currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + ?.getBoolean("incognito", false) ?: false AlertDialog.Builder(this, R.style.MyPopup) .setTitle(getString(R.string.title_update_progress)) .apply { @@ -818,7 +872,7 @@ class MangaReaderActivity : AppCompatActivity() { .setCancelable(false) .setPositiveButton(getString(R.string.yes)) { dialog, _ -> saveData("${media.id}_save_progress", true) - updateProgress( + updateProgress( media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!) .toString() diff --git a/app/src/main/res/layout/item_image.xml b/app/src/main/res/layout/item_image.xml index 64055928..72a66e12 100644 --- a/app/src/main/res/layout/item_image.xml +++ b/app/src/main/res/layout/item_image.xml @@ -7,7 +7,7 @@ app:gest_disableGestures="true" app:gest_rotationEnabled="true" app:gest_restrictRotation="true" - app:gest_doubleTapZoom="3" + app:gest_doubleTapZoom="1.5" app:gest_maxZoom="6"> Date: Sat, 30 Dec 2023 05:12:46 -0600 Subject: [PATCH 18/29] first working version of anime downloads --- app/src/main/AndroidManifest.xml | 2 +- .../ani/dantotsu/download/DownloadsManager.kt | 104 +++++++++-------- .../download/anime/AnimeDownloaderService.kt | 55 ++++++--- .../download/manga/MangaDownloaderService.kt | 6 +- .../download/manga/OfflineMangaFragment.kt | 43 ++++--- .../download/novel/NovelDownloaderService.kt | 6 +- ...Service.kt => ExoplayerDownloadService.kt} | 2 +- .../ani/dantotsu/download/video/Helper.kt | 90 ++++++++++++--- .../ani/dantotsu/media/anime/ExoplayerView.kt | 64 ++++++++--- .../dantotsu/media/manga/MangaReadFragment.kt | 12 +- .../dantotsu/media/novel/NovelReadFragment.kt | 14 +-- .../java/ani/dantotsu/parsers/AnimeSources.kt | 10 +- .../ani/dantotsu/parsers/AniyomiAdapter.kt | 15 +-- .../java/ani/dantotsu/parsers/BaseSources.kt | 13 +++ .../java/ani/dantotsu/parsers/MangaSources.kt | 3 - .../dantotsu/parsers/OfflineAnimeParser.kt | 106 ++++++++++++++++++ .../dantotsu/parsers/OfflineMangaParser.kt | 2 +- .../dantotsu/parsers/OfflineNovelParser.kt | 5 +- .../ani/dantotsu/parsers/VideoExtractor.kt | 6 +- 19 files changed, 402 insertions(+), 156 deletions(-) rename app/src/main/java/ani/dantotsu/download/video/{MyDownloadService.kt => ExoplayerDownloadService.kt} (91%) create mode 100644 app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 23f4b20d..8ab6c3ca 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -273,7 +273,7 @@ android:permission="android.permission.BIND_REMOTEVIEWS" android:exported="true" /> diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index b3536ea4..d6247ec5 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -15,43 +15,43 @@ class DownloadsManager(private val context: Context) { private val gson = Gson() private val downloadsList = loadDownloads().toMutableList() - val mangaDownloads: List - get() = downloadsList.filter { it.type == Download.Type.MANGA } - val animeDownloads: List - get() = downloadsList.filter { it.type == Download.Type.ANIME } - val novelDownloads: List - get() = downloadsList.filter { it.type == Download.Type.NOVEL } + val mangaDownloadedTypes: List + get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA } + val animeDownloadedTypes: List + get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME } + val novelDownloadedTypes: List + get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL } private fun saveDownloads() { val jsonString = gson.toJson(downloadsList) prefs.edit().putString("downloads_key", jsonString).apply() } - private fun loadDownloads(): List { + private fun loadDownloads(): List { val jsonString = prefs.getString("downloads_key", null) return if (jsonString != null) { - val type = object : TypeToken>() {}.type + val type = object : TypeToken>() {}.type gson.fromJson(jsonString, type) } else { emptyList() } } - fun addDownload(download: Download) { - downloadsList.add(download) + fun addDownload(downloadedType: DownloadedType) { + downloadsList.add(downloadedType) saveDownloads() } - fun removeDownload(download: Download) { - downloadsList.remove(download) - removeDirectory(download) + fun removeDownload(downloadedType: DownloadedType) { + downloadsList.remove(downloadedType) + removeDirectory(downloadedType) saveDownloads() } - fun removeMedia(title: String, type: Download.Type) { - val subDirectory = if (type == Download.Type.MANGA) { + fun removeMedia(title: String, type: DownloadedType.Type) { + val subDirectory = if (type == DownloadedType.Type.MANGA) { "Manga" - } else if (type == Download.Type.ANIME) { + } else if (type == DownloadedType.Type.ANIME) { "Anime" } else { "Novel" @@ -76,16 +76,16 @@ class DownloadsManager(private val context: Context) { } private fun cleanDownloads() { - cleanDownload(Download.Type.MANGA) - cleanDownload(Download.Type.ANIME) - cleanDownload(Download.Type.NOVEL) + cleanDownload(DownloadedType.Type.MANGA) + cleanDownload(DownloadedType.Type.ANIME) + cleanDownload(DownloadedType.Type.NOVEL) } - private fun cleanDownload(type: Download.Type) { + private fun cleanDownload(type: DownloadedType.Type) { // remove all folders that are not in the downloads list - val subDirectory = if (type == Download.Type.MANGA) { + val subDirectory = if (type == DownloadedType.Type.MANGA) { "Manga" - } else if (type == Download.Type.ANIME) { + } else if (type == DownloadedType.Type.ANIME) { "Anime" } else { "Novel" @@ -94,18 +94,18 @@ class DownloadsManager(private val context: Context) { context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/$subDirectory" ) - val downloadsSubList = if (type == Download.Type.MANGA) { - mangaDownloads - } else if (type == Download.Type.ANIME) { - animeDownloads + val downloadsSubLists = if (type == DownloadedType.Type.MANGA) { + mangaDownloadedTypes + } else if (type == DownloadedType.Type.ANIME) { + animeDownloadedTypes } else { - novelDownloads + novelDownloadedTypes } if (directory.exists()) { val files = directory.listFiles() if (files != null) { for (file in files) { - if (!downloadsSubList.any { it.title == file.name }) { + if (!downloadsSubLists.any { it.title == file.name }) { val deleted = file.deleteRecursively() } } @@ -122,7 +122,7 @@ class DownloadsManager(private val context: Context) { } } - fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List) //for debugging + fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List) //for debugging { val jsonString = gson.toJson(downloadsList) val file = File( @@ -138,25 +138,33 @@ class DownloadsManager(private val context: Context) { file.writeText(jsonString) } - fun queryDownload(download: Download): Boolean { - return downloadsList.contains(download) + fun queryDownload(downloadedType: DownloadedType): Boolean { + return downloadsList.contains(downloadedType) } - private fun removeDirectory(download: Download) { - val directory = if (download.type == Download.Type.MANGA) { + fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean { + return if (type == null) { + downloadsList.any { it.title == title && it.chapter == chapter } + } else { + downloadsList.any { it.title == title && it.chapter == chapter && it.type == type } + } + } + + private fun removeDirectory(downloadedType: DownloadedType) { + val directory = if (downloadedType.type == DownloadedType.Type.MANGA) { File( context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Manga/${download.title}/${download.chapter}" + "Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}" ) - } else if (download.type == Download.Type.ANIME) { + } else if (downloadedType.type == DownloadedType.Type.ANIME) { File( context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Anime/${download.title}/${download.chapter}" + "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}" ) } else { File( context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Novel/${download.title}/${download.chapter}" + "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}" ) } @@ -173,26 +181,26 @@ class DownloadsManager(private val context: Context) { } } - fun exportDownloads(download: Download) { //copies to the downloads folder available to the user - val directory = if (download.type == Download.Type.MANGA) { + fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user + val directory = if (downloadedType.type == DownloadedType.Type.MANGA) { File( context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Manga/${download.title}/${download.chapter}" + "Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}" ) - } else if (download.type == Download.Type.ANIME) { + } else if (downloadedType.type == DownloadedType.Type.ANIME) { File( context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Anime/${download.title}/${download.chapter}" + "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}" ) } else { File( context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Novel/${download.title}/${download.chapter}" + "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}" ) } val destination = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/${download.title}/${download.chapter}" + "Dantotsu/${downloadedType.title}/${downloadedType.chapter}" ) if (directory.exists()) { val copied = directory.copyRecursively(destination, true) @@ -206,10 +214,10 @@ class DownloadsManager(private val context: Context) { } } - fun purgeDownloads(type: Download.Type) { - val directory = if (type == Download.Type.MANGA) { + fun purgeDownloads(type: DownloadedType.Type) { + val directory = if (type == DownloadedType.Type.MANGA) { File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga") - } else if (type == Download.Type.ANIME) { + } else if (type == DownloadedType.Type.ANIME) { File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime") } else { File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel") @@ -237,7 +245,7 @@ class DownloadsManager(private val context: Context) { } -data class Download(val title: String, val chapter: String, val type: Type) : Serializable { +data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable { enum class Type { MANGA, ANIME, diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt index 256e9577..1519f8a8 100644 --- a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt @@ -8,7 +8,6 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.ServiceInfo -import android.graphics.Bitmap import android.os.Build import android.os.Environment import android.os.IBinder @@ -18,15 +17,15 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.offline.Download +import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadService import ani.dantotsu.R import ani.dantotsu.currActivity -import ani.dantotsu.download.Download +import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager -import ani.dantotsu.download.anime.AnimeDownloaderService -import ani.dantotsu.download.anime.AnimeServiceDataSingleton import ani.dantotsu.download.video.Helper -import ani.dantotsu.download.video.MyDownloadService +import ani.dantotsu.download.video.ExoplayerDownloadService import ani.dantotsu.logger import ani.dantotsu.media.Media import ani.dantotsu.media.anime.AnimeWatchFragment @@ -44,14 +43,11 @@ import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapterImpl import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -161,7 +157,7 @@ class AnimeDownloaderService : Service() { val url = AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url ?: "" DownloadService.sendRemoveDownload( this@AnimeDownloaderService, - MyDownloadService::class.java, + ExoplayerDownloadService::class.java, url, false ) @@ -220,16 +216,28 @@ class AnimeDownloaderService : Service() { } saveMediaInfo(task) - downloadsManager.addDownload( - Download( - task.title, - task.episode, - Download.Type.ANIME, - ) + var continueDownload = false + downloadManager.addListener( + object : androidx.media3.exoplayer.offline.DownloadManager.Listener { + override fun onDownloadChanged( + downloadManager: DownloadManager, + download: Download, + finalException: Exception? + ) { + continueDownload = true + } + } ) + //set an async timeout of 30 seconds before setting continueDownload to true + launch { + kotlinx.coroutines.delay(30000) + continueDownload = true + } + + // periodically check if the download is complete - while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) { + while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null || continueDownload == false) { val download = downloadManager.downloadIndex.getDownload(task.video.file.url) if (download != null) { if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) { @@ -251,6 +259,13 @@ class AnimeDownloaderService : Service() { task.getTaskName(), task.video.file.url ).apply() + downloadsManager.addDownload( + DownloadedType( + task.title, + task.episode, + DownloadedType.Type.ANIME, + ) + ) broadcastDownloadFinished(task.getTaskName()) break } @@ -284,9 +299,11 @@ class AnimeDownloaderService : Service() { GlobalScope.launch(Dispatchers.IO) { val directory = File( getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Anime/${task.title}" + "${DownloadsManager.animeLocation}/${task.title}" ) + val episodeDirectory = File(directory, task.episode) if (!directory.exists()) directory.mkdirs() + if (!episodeDirectory.exists()) episodeDirectory.mkdirs() val file = File(directory, "media.json") val gson = GsonBuilder() @@ -305,6 +322,9 @@ class AnimeDownloaderService : Service() { if (media != null) { media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") } media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") } + if (task.episodeImage != null) { + downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg") + } val jsonString = gson.toJson(media) withContext(Dispatchers.Main) { @@ -395,6 +415,7 @@ class AnimeDownloaderService : Service() { val video: Video, val subtitle: Subtitle? = null, val sourceMedia: Media? = null, + val episodeImage: String? = null, val retries: Int = 2, val simultaneousDownloads: Int = 2, ) { diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt index fce89d3e..202a4b8f 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt @@ -18,7 +18,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import ani.dantotsu.R -import ani.dantotsu.download.Download +import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.logger import ani.dantotsu.media.Media @@ -246,10 +246,10 @@ class MangaDownloaderService : Service() { saveMediaInfo(task) downloadsManager.addDownload( - Download( + DownloadedType( task.title, task.chapter, - Download.Type.MANGA + DownloadedType.Type.MANGA ) ) broadcastDownloadFinished(task.chapter) diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt index d405583c..1b7c031b 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -2,7 +2,6 @@ package ani.dantotsu.download.manga import android.animation.ObjectAnimator import android.content.Context -import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Build @@ -23,20 +22,16 @@ import android.widget.GridView import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView -import androidx.core.app.ActivityCompat.recreate import androidx.fragment.app.Fragment import ani.dantotsu.R -import ani.dantotsu.Refresh import ani.dantotsu.currActivity import ani.dantotsu.currContext -import ani.dantotsu.download.Download +import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager -import ani.dantotsu.initActivity import ani.dantotsu.logger import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.setSafeOnClickListener -import ani.dantotsu.settings.SettingsActivity import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.snackString import ani.dantotsu.statusBarHeight @@ -168,8 +163,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel val media = - downloadManager.mangaDownloads.firstOrNull { it.title == item.title } - ?: downloadManager.novelDownloads.firstOrNull { it.title == item.title } + downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title } + ?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title } media?.let { startActivity( Intent(requireContext(), MediaDetailsActivity::class.java) @@ -184,10 +179,10 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { gridView.setOnItemLongClickListener { parent, view, position, id -> // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel - val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) { - Download.Type.MANGA + val type: DownloadedType.Type = if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) { + DownloadedType.Type.MANGA } else { - Download.Type.NOVEL + DownloadedType.Type.NOVEL } // Alert dialog to confirm deletion val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup) @@ -292,19 +287,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { private fun getDownloads() { downloads = listOf() - val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct() + val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct() val newMangaDownloads = mutableListOf() for (title in mangaTitles) { - val _downloads = downloadManager.mangaDownloads.filter { it.title == title } + val _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title } val download = _downloads.first() val offlineMangaModel = loadOfflineMangaModel(download) newMangaDownloads += offlineMangaModel } downloads = newMangaDownloads - val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct() + val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct() val newNovelDownloads = mutableListOf() for (title in novelTitles) { - val _downloads = downloadManager.novelDownloads.filter { it.title == title } + val _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title } val download = _downloads.first() val offlineMangaModel = loadOfflineMangaModel(download) newNovelDownloads += offlineMangaModel @@ -313,17 +308,17 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { } - private fun getMedia(download: Download): Media? { - val type = if (download.type == Download.Type.MANGA) { + private fun getMedia(downloadedType: DownloadedType): Media? { + val type = if (downloadedType.type == DownloadedType.Type.MANGA) { "Manga" - } else if (download.type == Download.Type.ANIME) { + } else if (downloadedType.type == DownloadedType.Type.ANIME) { "Anime" } else { "Novel" } val directory = File( currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/$type/${download.title}" + "Dantotsu/$type/${downloadedType.title}" ) //load media.json and convert to media class with gson return try { @@ -343,23 +338,23 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { } } - private fun loadOfflineMangaModel(download: Download): OfflineMangaModel { - val type = if (download.type == Download.Type.MANGA) { + private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel { + val type = if (downloadedType.type == DownloadedType.Type.MANGA) { "Manga" - } else if (download.type == Download.Type.ANIME) { + } else if (downloadedType.type == DownloadedType.Type.ANIME) { "Anime" } else { "Novel" } val directory = File( currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/$type/${download.title}" + "Dantotsu/$type/${downloadedType.title}" ) //load media.json and convert to media class with gson try { val media = File(directory, "media.json") val mediaJson = media.readText() - val mediaModel = getMedia(download)!! + val mediaModel = getMedia(downloadedType)!! val cover = File(directory, "cover.jpg") val coverUri: Uri? = if (cover.exists()) { Uri.fromFile(cover) diff --git a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt index e987cd25..d729ef57 100644 --- a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt @@ -17,7 +17,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import ani.dantotsu.R -import ani.dantotsu.download.Download +import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.logger import ani.dantotsu.media.Media @@ -330,10 +330,10 @@ class NovelDownloaderService : Service() { saveMediaInfo(task) downloadsManager.addDownload( - Download( + DownloadedType( task.title, task.chapter, - Download.Type.NOVEL + DownloadedType.Type.NOVEL ) ) broadcastDownloadFinished(task.originalLink) diff --git a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt b/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt similarity index 91% rename from app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt rename to app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt index 8c0d56d2..3d7a1802 100644 --- a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt +++ b/app/src/main/java/ani/dantotsu/download/video/ExoplayerDownloadService.kt @@ -11,7 +11,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler import ani.dantotsu.R @UnstableApi -class MyDownloadService : DownloadService(1, 2000, "download_service", R.string.downloads, 0) { +class ExoplayerDownloadService : DownloadService(1, 2000, "download_service", R.string.downloads, 0) { companion object { private const val JOB_ID = 1 private const val FOREGROUND_NOTIFICATION_ID = 1 diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt index 056e96ee..4c10752d 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -3,6 +3,7 @@ package ani.dantotsu.download.video import android.Manifest import android.annotation.SuppressLint import android.app.Activity +import android.app.AlertDialog import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -12,6 +13,7 @@ import android.util.Log import androidx.annotation.OptIn import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getString import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes @@ -32,6 +34,8 @@ import androidx.media3.exoplayer.scheduler.Requirements import androidx.media3.ui.TrackSelectionDialogBuilder import ani.dantotsu.R import ani.dantotsu.defaultHeaders +import ani.dantotsu.download.DownloadedType +import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.anime.AnimeDownloaderService import ani.dantotsu.download.anime.AnimeServiceDataSingleton import ani.dantotsu.logError @@ -50,7 +54,8 @@ import java.util.concurrent.* object Helper { - var simpleCache: SimpleCache? = null + private var simpleCache: SimpleCache? = null + @SuppressLint("UnsafeOptInUsageError") fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) { val dataSourceFactory = DataSource.Factory { @@ -96,18 +101,18 @@ object Helper { ) downloadHelper.prepare(object : DownloadHelper.Callback { override fun onPrepared(helper: DownloadHelper) { - TrackSelectionDialogBuilder( - context, "Select thingy", helper.getTracks(0).groups + /*TrackSelectionDialogBuilder( TODO: use this for subtitles + context, "Select Source", helper.getTracks(0).groups ) { _, overrides -> val params = TrackSelectionParameters.Builder(context) overrides.forEach { params.addOverride(it.value) } helper.addTrackSelection(0, params.build()) - MyDownloadService + ExoplayerDownloadService DownloadService.sendAddDownload( context, - MyDownloadService::class.java, + ExoplayerDownloadService::class.java, helper.getDownloadRequest(null), false ) @@ -117,6 +122,14 @@ object Helper { if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)" } build().show() + }*/ + helper.getDownloadRequest(null).let { + DownloadService.sendAddDownload( + context, + ExoplayerDownloadService::class.java, + it, + false + ) } } @@ -149,7 +162,7 @@ object Helper { } val threadPoolSize = Runtime.getRuntime().availableProcessors() val executorService = Executors.newFixedThreadPool(threadPoolSize) - val downloadManager = DownloadManager( + val downloadManager = DownloadManager( context, database, getSimpleCache(context), @@ -160,15 +173,15 @@ object Helper { Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) maxParallelDownloads = 3 } - downloadManager.addListener( - object : DownloadManager.Listener { // Override methods of interest here. + downloadManager.addListener( //for testing + object : DownloadManager.Listener { override fun onDownloadChanged( downloadManager: DownloadManager, download: Download, finalException: Exception? ) { if (download.state == Download.STATE_COMPLETED) { - Log.e("Downloader", "Download Completed") + Log.e("Downloader", "Download Completed") } else if (download.state == Download.STATE_FAILED) { Log.e("Downloader", "Download Failed") } else if (download.state == Download.STATE_STOPPED) { @@ -199,6 +212,7 @@ object Helper { return downloadDirectory!! } + @OptIn(UnstableApi::class) fun startAnimeDownloadService( context: Context, title: String, @@ -224,16 +238,62 @@ object Helper { subtitle, sourceMedia ) - AnimeServiceDataSingleton.downloadQueue.offer(downloadTask) - if (!AnimeServiceDataSingleton.isServiceRunning) { - val intent = Intent(context, AnimeDownloaderService::class.java) - ContextCompat.startForegroundService(context, intent) - AnimeServiceDataSingleton.isServiceRunning = true + val downloadsManger = Injekt.get() + val downloadCheck = downloadsManger + .queryDownload(title, episode, DownloadedType.Type.ANIME) + + if (downloadCheck) { + AlertDialog.Builder(context) + .setTitle("Download Exists") + .setMessage("A download for this episode already exists. Do you want to overwrite it?") + .setPositiveButton("Yes") { _, _ -> + DownloadService.sendRemoveDownload( + context, + ExoplayerDownloadService::class.java, + context.getSharedPreferences( + getString(context, R.string.anime_downloads), + Context.MODE_PRIVATE + ).getString( + downloadTask.getTaskName(), + "" + ) ?: "", + false + ) + context.getSharedPreferences( + getString(context, R.string.anime_downloads), + Context.MODE_PRIVATE + ).edit() + .remove(downloadTask.getTaskName()) + .apply() + downloadsManger.removeDownload( + DownloadedType( + title, + episode, + DownloadedType.Type.ANIME + ) + ) + AnimeServiceDataSingleton.downloadQueue.offer(downloadTask) + if (!AnimeServiceDataSingleton.isServiceRunning) { + val intent = Intent(context, AnimeDownloaderService::class.java) + ContextCompat.startForegroundService(context, intent) + AnimeServiceDataSingleton.isServiceRunning = true + } + } + .setNegativeButton("No") { _, _ -> } + .show() + } else { + AnimeServiceDataSingleton.downloadQueue.offer(downloadTask) + if (!AnimeServiceDataSingleton.isServiceRunning) { + val intent = Intent(context, AnimeDownloaderService::class.java) + ContextCompat.startForegroundService(context, intent) + AnimeServiceDataSingleton.isServiceRunning = true + } } } - @OptIn(UnstableApi::class) private fun getSimpleCache(context: Context): SimpleCache { + @OptIn(UnstableApi::class) + fun getSimpleCache(context: Context): SimpleCache { return if (simpleCache == null) { val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) val database = Injekt.get() diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index 253080e2..8643d9de 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -4,6 +4,7 @@ import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.AlertDialog import android.app.Dialog +import android.app.DownloadManager import android.app.PictureInPictureParams import android.app.PictureInPictureUiState import android.content.ActivityNotFoundException @@ -97,7 +98,9 @@ import kotlin.math.min import kotlin.math.roundToInt import androidx.media3.cast.SessionAvailabilityListener import androidx.media3.cast.CastPlayer +import androidx.media3.exoplayer.offline.Download import androidx.mediarouter.app.MediaRouteButton +import ani.dantotsu.download.video.Helper import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext @@ -150,6 +153,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL private var orientationListener: OrientationEventListener? = null + private var downloadId: String? = null + companion object { var initialized = false lateinit var media: Media @@ -475,7 +480,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL if (isInitialized) { isPlayerPlaying = exoPlayer.isPlaying (exoPlay.drawable as Animatable?)?.start() - if (isPlayerPlaying || castPlayer.isPlaying ) { + if (isPlayerPlaying || castPlayer.isPlaying) { Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay) exoPlayer.pause() castPlayer.pause() @@ -1115,7 +1120,21 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL if (settings.cursedSpeeds) arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f) else - arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.15f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f) + arrayOf( + 0.25f, + 0.33f, + 0.5f, + 0.66f, + 0.75f, + 1f, + 1.15f, + 1.25f, + 1.33f, + 1.5f, + 1.66f, + 1.75f, + 2f + ) val speedsName = speeds.map { "${it}x" }.toTypedArray() var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed @@ -1292,7 +1311,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL if (video?.format == VideoType.CONTAINER || (loadData("settings_download_manager") ?: 0) != 0 ) { - but.visibility = View.VISIBLE + //but.visibility = View.VISIBLE TODO: not sure if this is needed but.setOnClickListener { download(this, episode, animeTitle.text.toString()) } @@ -1317,8 +1336,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL dataSource } cacheFactory = CacheDataSource.Factory().apply { - setCache(simpleCache) + setCache(Helper.getSimpleCache(this@ExoplayerView)) setUpstreamDataSourceFactory(dataSourceFactory) + setCacheWriteDataSinkFactory(null) } val mimeType = when (video?.format) { @@ -1327,15 +1347,33 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL else -> MimeTypes.APPLICATION_MP4 } - val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) - logger("url: ${video!!.file.url}") - logger("mimeType: $mimeType") + val downloadedMediaItem = if (ext.server.offline) { + val key = ext.server.name + downloadId = getSharedPreferences(getString(R.string.anime_downloads), MODE_PRIVATE) + .getString(key, null) + if (downloadId != null) { + Helper.downloadManager(this) + .downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem() + } else { + snackString("Download not found") + null + } + } else null - if (sub != null) { - val listofnotnullsubs = immutableListOf(sub).filterNotNull() - builder.setSubtitleConfigurations(listofnotnullsubs) + mediaItem = if (downloadedMediaItem == null) { + val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) + logger("url: ${video!!.file.url}") + logger("mimeType: $mimeType") + + if (sub != null) { + val listofnotnullsubs = immutableListOf(sub).filterNotNull() + builder.setSubtitleConfigurations(listofnotnullsubs) + } + builder.build() + } else { + downloadedMediaItem } - mediaItem = builder.build() + //Source exoSource.setOnClickListener { @@ -1457,7 +1495,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL exoPlayer.release() VideoCache.release() mediaSession?.release() - if(DiscordServiceRunningSingleton.running) { + if (DiscordServiceRunningSingleton.running) { val stopIntent = Intent(this, DiscordService::class.java) DiscordServiceRunningSingleton.running = false stopService(stopIntent) @@ -1594,7 +1632,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL if (isInitialized) { if (exoPlayer.currentPosition.toFloat() / exoPlayer.duration > settings.watchPercentage) { preloading = true - nextEpisode(false) { i -> + nextEpisode(false) { i -> //TODO: make sure this works for offline episodes val ep = episodes[episodeArr[currentEpisodeIndex + i]] ?: return@nextEpisode val selected = media.selected ?: return@nextEpisode lifecycleScope.launch(Dispatchers.IO) { 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 9b70ef7f..241c979a 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -29,7 +29,7 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.* import ani.dantotsu.databinding.FragmentAnimeWatchBinding -import ani.dantotsu.download.Download +import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.manga.MangaDownloaderService import ani.dantotsu.download.manga.MangaServiceDataSingleton @@ -166,7 +166,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this) - for (download in downloadManager.mangaDownloads) { + for (download in downloadManager.mangaDownloadedTypes) { chapterAdapter.stopDownload(download.chapter) } @@ -482,10 +482,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { fun onMangaChapterRemoveDownloadClick(i: String) { downloadManager.removeDownload( - Download( + DownloadedType( media.nameMAL ?: media.nameRomaji, i, - Download.Type.MANGA + DownloadedType.Type.MANGA ) ) chapterAdapter.deleteDownload(i) @@ -500,10 +500,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { // Remove the download from the manager and update the UI downloadManager.removeDownload( - Download( + DownloadedType( media.nameMAL ?: media.nameRomaji, i, - Download.Type.MANGA + DownloadedType.Type.MANGA ) ) chapterAdapter.purgeDownload(i) diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt index 7154412a..b3e14ed2 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.databinding.FragmentAnimeWatchBinding -import ani.dantotsu.download.Download +import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.novel.NovelDownloaderService import ani.dantotsu.download.novel.NovelServiceDataSingleton @@ -92,10 +92,10 @@ class NovelReadFragment : Fragment(), override fun downloadedCheckWithStart(novel: ShowResponse): Boolean { val downloadsManager = Injekt.get() if (downloadsManager.queryDownload( - Download( + DownloadedType( media.nameMAL ?: media.nameRomaji, novel.name, - Download.Type.NOVEL + DownloadedType.Type.NOVEL ) ) ) { @@ -124,10 +124,10 @@ class NovelReadFragment : Fragment(), override fun downloadedCheck(novel: ShowResponse): Boolean { val downloadsManager = Injekt.get() return downloadsManager.queryDownload( - Download( + DownloadedType( media.nameMAL ?: media.nameRomaji, novel.name, - Download.Type.NOVEL + DownloadedType.Type.NOVEL ) ) } @@ -135,10 +135,10 @@ class NovelReadFragment : Fragment(), override fun deleteDownload(novel: ShowResponse) { val downloadsManager = Injekt.get() downloadsManager.removeDownload( - Download( + DownloadedType( media.nameMAL ?: media.nameRomaji, novel.name, - Download.Type.NOVEL + DownloadedType.Type.NOVEL ) ) } diff --git a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt index c48a4b3e..dd3faaef 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt @@ -12,11 +12,17 @@ object AnimeSources : WatchSources() { suspend fun init(fromExtensions: StateFlow>) { // Initialize with the first value from StateFlow val initialExtensions = fromExtensions.first() - list = createParsersFromExtensions(initialExtensions) + list = createParsersFromExtensions(initialExtensions) + Lazier( + { OfflineAnimeParser() }, + "Downloaded" + ) // Update as StateFlow emits new values fromExtensions.collect { extensions -> - list = createParsersFromExtensions(extensions) + list = createParsersFromExtensions(extensions) + Lazier( + { OfflineAnimeParser() }, + "Downloaded" + ) } } diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 27aece5f..08c69b55 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -616,17 +616,18 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { val fileName = queryPairs.find { it.first == "file" }?.second ?: "" format = getVideoType(fileName) - if (format == null) { - val networkHelper = Injekt.get() - format = headRequest(videoUrl, networkHelper) - } + // this solves a problem no one has, so I'm commenting it out for now + //if (format == null) { + // val networkHelper = Injekt.get() + // format = headRequest(videoUrl, networkHelper) + //} } - // If the format is still undetermined, log an error or handle it appropriately + // If the format is still undetermined, log an error if (format == null) { logger("Unknown video format: $videoUrl") - FirebaseCrashlytics.getInstance() - .recordException(Exception("Unknown video format: $videoUrl")) + //FirebaseCrashlytics.getInstance() + // .recordException(Exception("Unknown video format: $videoUrl")) format = VideoType.CONTAINER } val headersMap: Map = diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt index f8535772..95fd4d63 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt @@ -46,6 +46,19 @@ abstract class WatchSources : BaseSources() { sEpisode = it.sEpisode ) } + } else if (parser is OfflineAnimeParser) { + parser.loadEpisodes(showLink, extra, SAnime.create()).forEach { + map[it.number] = Episode( + it.number, + it.link, + it.title, + it.description, + it.thumbnail, + it.isFiller, + extra = it.extra, + sEpisode = it.sEpisode + ) + } } } return map diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt index 0f8a5642..3709ddaa 100644 --- a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt @@ -7,9 +7,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first object MangaSources : MangaReadSources() { - // Instantiate the static parser - private val offlineMangaParser by lazy { OfflineMangaParser() } - override var list: List> = emptyList() suspend fun init(fromExtensions: StateFlow>) { diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt new file mode 100644 index 00000000..dca2199e --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt @@ -0,0 +1,106 @@ +package ani.dantotsu.parsers + +import android.os.Environment +import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.logger +import ani.dantotsu.media.anime.AnimeNameAdapter +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SEpisode +import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl +import me.xdrop.fuzzywuzzy.FuzzySearch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File + +class OfflineAnimeParser : AnimeParser() { + private val downloadManager = Injekt.get() + + override val name = "Offline" + override val saveName = "Offline" + override val hostUrl = "Offline" + override val isDubAvailableSeparately = false + override val isNSFW = false + + override suspend fun loadEpisodes( + animeLink: String, + extra: Map?, + sAnime: SAnime + ): List { + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "${DownloadsManager.animeLocation}/$animeLink" + ) + //get all of the folder names and add them to the list + val episodes = mutableListOf() + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isDirectory) { + val episode = Episode( + it.name, + "$animeLink - ${it.name}", + it.name, + null, + null, + sEpisode = SEpisodeImpl() + ) + episodes.add(episode) + } + } + episodes.sortBy { AnimeNameAdapter.findEpisodeNumber(it.number) } + return episodes + } + return emptyList() + } + + override suspend fun loadVideoServers( + episodeLink: String, + extra: Map?, + sEpisode: SEpisode + ): List { + return listOf( + VideoServer( + episodeLink, + offline = true + ) + ) + } + + + override suspend fun search(query: String): List { + val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct() + val returnTitles: MutableList = mutableListOf() + for (title in titles) { + if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { + returnTitles.add(title) + } + } + val returnList: MutableList = mutableListOf() + for (title in returnTitles) { + returnList.add(ShowResponse(title, title, title)) + } + return returnList + } + + override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor { + return OfflineVideoExtractor(server) + } + +} + +class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() { + override val server: VideoServer + get() = videoServer + + override suspend fun extract(): VideoContainer { + val sublist = emptyList() + //we need to return a "fake" video so that the app doesn't crash + val video = Video( + null, + VideoType.CONTAINER, + "", + ) + return VideoContainer(listOf(video), sublist) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt index 218a0f9a..deffb420 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt @@ -76,7 +76,7 @@ class OfflineMangaParser : MangaParser() { } override suspend fun search(query: String): List { - val titles = downloadManager.mangaDownloads.map { it.title }.distinct() + val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct() val returnTitles: MutableList = mutableListOf() for (title in titles) { if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt index ca47b50a..fece57e2 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt @@ -3,10 +3,7 @@ package ani.dantotsu.parsers import android.os.Environment import ani.dantotsu.currContext import ani.dantotsu.download.DownloadsManager -import ani.dantotsu.logger import ani.dantotsu.media.manga.MangaNameAdapter -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga import me.xdrop.fuzzywuzzy.FuzzySearch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -53,7 +50,7 @@ class OfflineNovelParser: NovelParser() { } override suspend fun search(query: String): List { - val titles = downloadManager.novelDownloads.map { it.title }.distinct() + val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct() val returnTitles: MutableList = mutableListOf() for (title in titles) { if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { diff --git a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt index ce56182c..c98b8a42 100644 --- a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt +++ b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt @@ -57,11 +57,15 @@ data class VideoServer( val name: String, val embed: FileUrl, val extraData: Map? = null, - val video: eu.kanade.tachiyomi.animesource.model.Video? = null + val video: eu.kanade.tachiyomi.animesource.model.Video? = null, + val offline: Boolean = false ) : Serializable { constructor(name: String, embedUrl: String, extraData: Map? = null) : this(name, FileUrl(embedUrl), extraData) + constructor(name: String, offline: Boolean) + : this(name, FileUrl(""), null, null, offline) + constructor( name: String, embedUrl: String, From 7fae64bee91cedb27f0033d2c918283f9b0e8df3 Mon Sep 17 00:00:00 2001 From: Sadwhy <99601717+Sadwhy@users.noreply.github.com> Date: Sat, 30 Dec 2023 17:13:15 +0600 Subject: [PATCH 19/29] Ignore readme on workflow (#107) --- .github/workflows/beta.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 1f24338c..e54229eb 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -4,6 +4,8 @@ on: push: branches: - dev + paths-ignore: + - '**/README.md' jobs: build: From 7dbf951d5a2b56a4c557a269a5c3717822b77b22 Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Sun, 31 Dec 2023 00:17:18 -0600 Subject: [PATCH 20/29] download episode images --- .../main/java/ani/dantotsu/MainActivity.kt | 23 +++++--- .../download/anime/AnimeDownloaderService.kt | 58 ++++++++++++------- .../ani/dantotsu/download/video/Helper.kt | 6 +- .../media/anime/SelectorDialogFragment.kt | 3 +- 4 files changed, 56 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index f58944f7..50d681c5 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.offline.Download import androidx.viewpager2.adapter.FragmentStateAdapter import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistHomeViewModel @@ -249,15 +250,19 @@ class MainActivity : AppCompatActivity() { GlobalScope.launch(Dispatchers.IO) { val index = Helper.downloadManager(this@MainActivity).downloadIndex - if (index != null) { - val downloadCursor = index.getDownloads() - if (downloadCursor != null) { - while (downloadCursor.moveToNext()) { - val download = downloadCursor.download - Log.e("Downloader", download.request.uri.toString()) - Log.e("Downloader", download.request.id.toString()) - Log.e("Downloader", download.request.mimeType.toString()) - } + val downloadCursor = index.getDownloads() + while (downloadCursor.moveToNext()) { + val download = downloadCursor.download + Log.e("Downloader", download.request.uri.toString()) + Log.e("Downloader", download.request.id.toString()) + Log.e("Downloader", download.request.mimeType.toString()) + Log.e("Downloader", download.request.data.size.toString()) + Log.e("Downloader", download.bytesDownloaded.toString()) + Log.e("Downloader", download.state.toString()) + Log.e("Downloader", download.failureReason.toString()) + + if (download.state == Download.STATE_FAILED) { //simple cleanup + Helper.downloadManager(this@MainActivity).removeDownload(download.request.id) } } } diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt index 1519f8a8..84a960cc 100644 --- a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt @@ -20,6 +20,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadService +import ani.dantotsu.FileUrl import ani.dantotsu.R import ani.dantotsu.currActivity import ani.dantotsu.download.DownloadedType @@ -84,7 +85,6 @@ class AnimeDownloaderService : Service() { setSmallIcon(R.drawable.ic_round_download_24) priority = NotificationCompat.PRIORITY_DEFAULT setOnlyAlertOnce(true) - setProgress(0, 0, false) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground( @@ -216,43 +216,39 @@ class AnimeDownloaderService : Service() { } saveMediaInfo(task) - var continueDownload = false - downloadManager.addListener( - object : androidx.media3.exoplayer.offline.DownloadManager.Listener { - override fun onDownloadChanged( - downloadManager: DownloadManager, - download: Download, - finalException: Exception? - ) { - continueDownload = true - } - } - ) + val downloadStarted = hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout - //set an async timeout of 30 seconds before setting continueDownload to true - launch { - kotlinx.coroutines.delay(30000) - continueDownload = true + if (!downloadStarted) { + logger("Download failed to start") + builder.setContentText("${task.title} - ${task.episode} Download failed to start") + notificationManager.notify(NOTIFICATION_ID, builder.build()) + snackString("${task.title} - ${task.episode} Download failed to start") + broadcastDownloadFailed(task.getTaskName()) + return@withContext } // periodically check if the download is complete - while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null || continueDownload == false) { + while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) { val download = downloadManager.downloadIndex.getDownload(task.video.file.url) if (download != null) { if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) { logger("Download failed") builder.setContentText("${task.title} - ${task.episode} Download failed") - .setProgress(0, 0, false) notificationManager.notify(NOTIFICATION_ID, builder.build()) snackString("${task.title} - ${task.episode} Download failed") + logger("Download failed: ${download.failureReason}") + FirebaseCrashlytics.getInstance().recordException(Exception("Anime Download failed:" + + " ${download.failureReason}" + + " url: ${task.video.file.url}" + + " title: ${task.title}" + + " episode: ${task.episode}")) broadcastDownloadFailed(task.getTaskName()) break } if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) { logger("Download completed") builder.setContentText("${task.title} - ${task.episode} Download completed") - .setProgress(0, 0, false) notificationManager.notify(NOTIFICATION_ID, builder.build()) snackString("${task.title} - ${task.episode} Download completed") getSharedPreferences(getString(R.string.anime_downloads), Context.MODE_PRIVATE).edit().putString( @@ -272,13 +268,11 @@ class AnimeDownloaderService : Service() { if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) { logger("Download stopped") builder.setContentText("${task.title} - ${task.episode} Download stopped") - .setProgress(0, 0, false) notificationManager.notify(NOTIFICATION_ID, builder.build()) snackString("${task.title} - ${task.episode} Download stopped") break } broadcastDownloadProgress(task.getTaskName(), download.percentDownloaded.toInt()) - builder.setProgress(100, download.percentDownloaded.toInt(), false) if (notifi) { notificationManager.notify(NOTIFICATION_ID, builder.build()) } @@ -294,6 +288,19 @@ class AnimeDownloaderService : Service() { } } + @androidx.annotation.OptIn(UnstableApi::class) suspend fun hasDownloadStarted(downloadManager: DownloadManager, task: DownloadTask, timeout: Long): Boolean { + val startTime = System.currentTimeMillis() + while (System.currentTimeMillis() - startTime < timeout) { + val download = downloadManager.downloadIndex.getDownload(task.video.file.url) + if (download != null) { + return true + } + // Delay between each poll + kotlinx.coroutines.delay(500) + } + return false + } + @OptIn(DelicateCoroutinesApi::class) private fun saveMediaInfo(task: DownloadTask) { GlobalScope.launch(Dispatchers.IO) { @@ -323,6 +330,13 @@ class AnimeDownloaderService : Service() { media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") } media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") } if (task.episodeImage != null) { + media.anime?.episodes?.get(task.episode)?.let { episode -> + episode.thumb = downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg")?.let { + FileUrl( + it + ) + } + } downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg") } diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt index 4c10752d..8d8b52c6 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -219,7 +219,8 @@ object Helper { episode: String, video: Video, subtitle: Subtitle? = null, - sourceMedia: Media? = null + sourceMedia: Media? = null, + episodeImage: String? = null ) { if (!isNotificationPermissionGranted(context)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -236,7 +237,8 @@ object Helper { episode, video, subtitle, - sourceMedia + sourceMedia, + episodeImage ) val downloadsManger = Injekt.get() diff --git a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt index c9d734f4..e90abecb 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -283,7 +283,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { episode.number, video, null, - media + media, + episode.thumb?.url?: media!!.banner?: media!!.cover ) } dismiss() From 7228817c68d800dc61e3ce6c67942f53aeb1de8b Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Sun, 31 Dec 2023 00:55:39 -0600 Subject: [PATCH 21/29] I have no idea why that crashes for some people... --- app/src/main/java/ani/dantotsu/Functions.kt | 56 ++++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt index 2ac075f0..a1035d05 100644 --- a/app/src/main/java/ani/dantotsu/Functions.kt +++ b/app/src/main/java/ani/dantotsu/Functions.kt @@ -53,6 +53,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.internal.ViewUtils import com.google.android.material.snackbar.Snackbar +import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlinx.coroutines.* import nl.joery.animatedbottombar.AnimatedBottomBar import java.io.* @@ -783,35 +784,40 @@ fun toast(string: String?) { } fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) { - if (s != null) { - (activity ?: currActivity())?.apply { - runOnUiThread { - val snackBar = Snackbar.make( - window.decorView.findViewById(android.R.id.content), - s, - Snackbar.LENGTH_SHORT - ) - snackBar.view.apply { - updateLayoutParams { - gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) - width = WRAP_CONTENT - } - translationY = -(navBarHeight.dp + 32f) - translationZ = 32f - updatePadding(16f.px, right = 16f.px) - setOnClickListener { - snackBar.dismiss() - } - setOnLongClickListener { - copyToClipboard(clipboard ?: s, false) - toast(getString(R.string.copied_to_clipboard)) - true + try { //I have no idea why this sometimes crashes for some people... + if (s != null) { + (activity ?: currActivity())?.apply { + runOnUiThread { + val snackBar = Snackbar.make( + window.decorView.findViewById(android.R.id.content), + s, + Snackbar.LENGTH_SHORT + ) + snackBar.view.apply { + updateLayoutParams { + gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) + width = WRAP_CONTENT + } + translationY = -(navBarHeight.dp + 32f) + translationZ = 32f + updatePadding(16f.px, right = 16f.px) + setOnClickListener { + snackBar.dismiss() + } + setOnLongClickListener { + copyToClipboard(clipboard ?: s, false) + toast(getString(R.string.copied_to_clipboard)) + true + } } + snackBar.show() } - snackBar.show() } + logger(s) } - logger(s) + } catch (e: Exception) { + logger(e.stackTraceToString()) + FirebaseCrashlytics.getInstance().recordException(e) } } From 98a3a1107b2820bbb3cbd9631062eec748957475 Mon Sep 17 00:00:00 2001 From: aayush262 <99584765+aayush2622@users.noreply.github.com> Date: Sun, 31 Dec 2023 12:55:03 +0530 Subject: [PATCH 22/29] =?UTF-8?q?Title=F0=9F=98=82=F0=9F=98=82=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * small changes * Changes * new nest button for settings * full language name in language selector * tv banner * hide lang selector if there is one language only * and some small changes * import fix * alter dialog * wont refresh if nothing is changed --- app/src/main/AndroidManifest.xml | 2 +- .../download/manga/OfflineMangaFragment.kt | 3 + .../ani/dantotsu/download/video/Helper.kt | 2 +- .../ani/dantotsu/home/AnimePageAdapter.kt | 2 - .../ani/dantotsu/home/MangaPageAdapter.kt | 3 - .../dantotsu/media/anime/AnimeWatchAdapter.kt | 128 ++++++---- .../media/anime/CustomCastThemeFactory.kt | 4 +- .../dantotsu/media/manga/MangaReadAdapter.kt | 222 ++++++++++------- .../ani/dantotsu/others/LanguageMapper.kt | 30 ++- .../others/imagesearch/ImageSearchActivity.kt | 2 + .../res/drawable-v24/ic_banner_foreground.xml | 67 ------ .../main/res/drawable/ic_round_filter_24.xml | 4 + .../main/res/layout/activity_image_search.xml | 4 +- app/src/main/res/layout/dialog_layout.xml | 225 ++++++++++++++++++ app/src/main/res/layout/item_anime_watch.xml | 98 +++----- .../main/res/mipmap-anydpi-v26/ic_banner.xml | 2 +- app/src/main/res/mipmap-xhdpi/ic_banner.png | Bin 5245 -> 21750 bytes .../res/mipmap-xhdpi/ic_banner_foreground.png | Bin 0 -> 21750 bytes app/src/main/res/values/style.xml | 2 +- 19 files changed, 516 insertions(+), 284 deletions(-) delete mode 100644 app/src/main/res/drawable-v24/ic_banner_foreground.xml create mode 100644 app/src/main/res/drawable/ic_round_filter_24.xml create mode 100644 app/src/main/res/layout/dialog_layout.xml create mode 100644 app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8ab6c3ca..c0a14ae7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,7 +47,7 @@ = Build.VERSION_CODES.P) { val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt index 8d8b52c6..48909107 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -246,7 +246,7 @@ object Helper { .queryDownload(title, episode, DownloadedType.Type.ANIME) if (downloadCheck) { - AlertDialog.Builder(context) + AlertDialog.Builder(context , R.style.MyPopup) .setTitle("Download Exists") .setMessage("A download for this episode already exists. Do you want to overwrite it?") .setPositiveButton("Yes") { _, _ -> diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index 732ac43f..d843b2dc 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -75,8 +75,6 @@ class AnimePageAdapter : RecyclerView.Adapter { 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 ea30fa58..c6a2240a 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -1,23 +1,28 @@ package ani.dantotsu.media.anime import android.annotation.SuppressLint +import android.content.Context import android.content.Intent import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter +import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout +import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.* import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemChipBinding +import ani.dantotsu.databinding.DialogLayoutBinding import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.SourceSearchDialogFragment +import ani.dantotsu.others.LanguageMapper import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.DynamicAnimeParser import ani.dantotsu.parsers.WatchSources @@ -27,6 +32,7 @@ import com.google.android.material.chip.Chip import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch + class AnimeWatchAdapter( private val media: Media, private val fragment: AnimeWatchFragment, @@ -40,6 +46,8 @@ class AnimeWatchAdapter( val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(bind) } + private var nestedDialog: AlertDialog? = null + @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: ViewHolder, position: Int) { @@ -147,8 +155,9 @@ class AnimeWatchAdapter( } } + //Icons - //Subscription + //subscribe subscribe = MediaDetailsActivity.PopImageButton( fragment.lifecycleScope, binding.animeSourceSubscribe, @@ -167,44 +176,76 @@ class AnimeWatchAdapter( openSettings(fragment.requireContext(), getChannelId(true, media.id)) } - //Icons - var reversed = media.selected!!.recyclerReversed - var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView - binding.animeSourceTop.rotation = if (reversed) -90f else 90f - binding.animeSourceTop.setOnClickListener { - reversed = !reversed - binding.animeSourceTop.rotation = if (reversed) -90f else 90f - fragment.onIconPressed(style, reversed) - } - var selected = when (style) { - 0 -> binding.animeSourceList - 1 -> binding.animeSourceGrid - 2 -> binding.animeSourceCompact - else -> binding.animeSourceList - } - selected.alpha = 1f - fun selected(it: ImageView) { - selected.alpha = 0.33f - selected = it + //Nested Button + binding.animeNestedButton.setOnClickListener { + val dialogView = + LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null) + val dialogBinding = DialogLayoutBinding.bind(dialogView) + + var run = false + var reversed = media.selected!!.recyclerReversed + var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView + dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f + dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" + dialogBinding.animeSourceTop.setOnClickListener { + reversed = !reversed + dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f + dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" + run = true + } + //Grids + var selected = when (style) { + 0 -> dialogBinding.animeSourceList + 1 -> dialogBinding.animeSourceGrid + 2 -> dialogBinding.animeSourceCompact + else -> dialogBinding.animeSourceList + } + when (style) { + 0 -> dialogBinding.layoutText.text = "List" + 1 -> dialogBinding.layoutText.text = "Grid" + 2 -> dialogBinding.layoutText.text = "Compact" + else -> dialogBinding.animeSourceList + } selected.alpha = 1f + fun selected(it: ImageButton) { + selected.alpha = 0.33f + selected = it + selected.alpha = 1f + } + dialogBinding.animeSourceList.setOnClickListener { + selected(it as ImageButton) + style = 0 + dialogBinding.layoutText.text = "List" + run = true + } + dialogBinding.animeSourceGrid.setOnClickListener { + selected(it as ImageButton) + style = 1 + dialogBinding.layoutText.text = "Grid" + run = true + } + dialogBinding.animeSourceCompact.setOnClickListener { + selected(it as ImageButton) + style = 2 + dialogBinding.layoutText.text = "Compact" + run = true + } + + //hidden + dialogBinding.animeScanlatorContainer.visibility = View.GONE + dialogBinding.animeDownloadContainer.visibility = View.GONE + + nestedDialog = AlertDialog.Builder(fragment.requireContext() , R.style.MyPopup) + .setTitle("Options") + .setView(dialogView) + .setPositiveButton("OK") { _, _ -> + if (run) fragment.onIconPressed(style, reversed) + } + .setNegativeButton("Cancel") { _, _ -> + } + .create() + nestedDialog?.show() } - binding.animeSourceList.setOnClickListener { - selected(it as ImageView) - style = 0 - fragment.onIconPressed(style, reversed) - } - binding.animeSourceGrid.setOnClickListener { - selected(it as ImageView) - style = 1 - fragment.onIconPressed(style, reversed) - } - binding.animeSourceCompact.setOnClickListener { - selected(it as ImageView) - style = 2 - fragment.onIconPressed(style, reversed) - } - binding.animeScanlatorTop.visibility = View.GONE - binding.animeDownloadTop.visibility = View.GONE //Episode Handling handleEpisodes() } @@ -351,12 +392,15 @@ class AnimeWatchAdapter( parser.extension.sources.firstOrNull()?.lang ?: "Unknown" ) } - binding?.animeSourceLanguage?.setAdapter( - ArrayAdapter( - fragment.requireContext(), - R.layout.item_dropdown, - parser.extension.sources.map { it.lang }) + val adapter = ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) } ) + val items = adapter.count + if (items > 1) binding?.animeSourceLanguageContainer?.visibility = View.VISIBLE else binding?.animeSourceLanguageContainer?.visibility = View.GONE + + binding?.animeSourceLanguage?.setAdapter(adapter) } } diff --git a/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt b/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt index 850072b8..72239043 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/CustomCastThemeFactory.kt @@ -31,7 +31,7 @@ class CustomMediaRouterChooserDialogFragment: MediaRouteChooserDialogFragment() context: Context, savedInstanceState: Bundle? ): MediaRouteChooserDialog = - MediaRouteChooserDialog(context) + MediaRouteChooserDialog(context, R.style.MyPopup) } class CustomMediaRouteControllerDialogFragment: MediaRouteControllerDialogFragment() { @@ -39,5 +39,5 @@ class CustomMediaRouteControllerDialogFragment: MediaRouteControllerDialogFragme context: Context, savedInstanceState: Bundle? ): MediaRouteControllerDialog = - MediaRouteControllerDialog(context, R.style.ThemeOverlay_Dantotsu_MediaRouter) + MediaRouteControllerDialog(context, R.style.MyPopup) } \ No newline at end of file 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 342dfe20..8aa03489 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt @@ -2,12 +2,14 @@ package ani.dantotsu.media.manga import android.annotation.SuppressLint import android.app.AlertDialog +import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.ArrayAdapter import android.widget.CheckBox +import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.NumberPicker @@ -15,12 +17,14 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.* +import ani.dantotsu.databinding.DialogLayoutBinding 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.media.anime.handleProgress +import ani.dantotsu.others.LanguageMapper import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaSources @@ -30,6 +34,7 @@ import com.google.android.material.chip.Chip import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch + class MangaReadAdapter( private val media: Media, private val fragment: MangaReadFragment, @@ -46,7 +51,7 @@ class MangaReadAdapter( val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(bind) } - + private var nestedDialog: AlertDialog? = null @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: ViewHolder, position: Int) { val binding = holder.binding @@ -117,7 +122,7 @@ class MangaReadAdapter( } } - //Subscription + //Grids subscribe = MediaDetailsActivity.PopImageButton( fragment.lifecycleScope, binding.animeSourceSubscribe, @@ -136,98 +141,130 @@ class MangaReadAdapter( openSettings(fragment.requireContext(), getChannelId(true, media.id)) } - //Icons - binding.animeSourceGrid.visibility = View.GONE - var reversed = media.selected!!.recyclerReversed - var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView - binding.animeSourceTop.rotation = if (reversed) -90f else 90f - binding.animeSourceTop.setOnClickListener { - reversed = !reversed - binding.animeSourceTop.rotation = if (reversed) -90f else 90f - fragment.onIconPressed(style, reversed) - } + binding.animeNestedButton.setOnClickListener { - binding.animeScanlatorTop.setOnClickListener { val dialogView = - LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) - val checkboxContainer = dialogView.findViewById(R.id.checkboxContainer) + LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null) + val dialogBinding = DialogLayoutBinding.bind(dialogView) - // Dynamically add checkboxes - - options.forEach { option -> - val checkBox = CheckBox(currContext()).apply { - text = option - } - //set checked if it's already selected - if (media.selected!!.scanlators != null) { - checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true - scanlatorSelectionListener?.onScanlatorsSelected() - } else { - checkBox.isChecked = true - } - checkboxContainer.addView(checkBox) + var run = false + var reversed = media.selected!!.recyclerReversed + var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView + dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f + dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" + dialogBinding.animeSourceTop.setOnClickListener { + reversed = !reversed + dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f + dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" + run = true } - // Create AlertDialog - val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup) - .setView(dialogView) - .setPositiveButton("OK") { dialog, which -> - //add unchecked to hidden - hiddenScanlators.clear() - for (i in 0 until checkboxContainer.childCount) { - val checkBox = checkboxContainer.getChildAt(i) as CheckBox - if (!checkBox.isChecked) { - hiddenScanlators.add(checkBox.text.toString()) - } - } - fragment.onScanlatorChange(hiddenScanlators) - scanlatorSelectionListener?.onScanlatorsSelected() - } - .setNegativeButton("Cancel", null) - .show() - dialog.window?.setDimAmount(0.8f) - } - - binding.animeDownloadTop.setOnClickListener { - //Alert dialog asking for the number of chapters to download - val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup) - alertDialog.setTitle("Multi Chapter Downloader") - alertDialog.setMessage("Enter the number of chapters to download") - val input = NumberPicker(currContext()) - input.minValue = 1 - input.maxValue = 20 - input.value = 1 - alertDialog.setView(input) - alertDialog.setPositiveButton("OK") { dialog, which -> - fragment.multiDownload(input.value) + //Grids + dialogBinding.animeSourceGrid.visibility = View.GONE + var selected = when (style) { + 0 -> dialogBinding.animeSourceList + 1 -> dialogBinding.animeSourceCompact + else -> dialogBinding.animeSourceList + } + when (style) { + 0 -> dialogBinding.layoutText.text = "List" + 1 -> dialogBinding.layoutText.text = "Compact" + else -> dialogBinding.animeSourceList } - alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } - val dialog = alertDialog.show() - dialog.window?.setDimAmount(0.8f) - } - - var selected = when (style) { - 0 -> binding.animeSourceList - 1 -> binding.animeSourceCompact - else -> binding.animeSourceList - } - selected.alpha = 1f - fun selected(it: ImageView) { - selected.alpha = 0.33f - selected = it selected.alpha = 1f - } - binding.animeSourceList.setOnClickListener { - selected(it as ImageView) - style = 0 - fragment.onIconPressed(style, reversed) - } - binding.animeSourceCompact.setOnClickListener { - selected(it as ImageView) - style = 1 - fragment.onIconPressed(style, reversed) - } + fun selected(it: ImageButton) { + selected.alpha = 0.33f + selected = it + selected.alpha = 1f + } + dialogBinding.animeSourceList.setOnClickListener { + selected(it as ImageButton) + style = 0 + dialogBinding.layoutText.text = "List" + run = true + } + dialogBinding.animeSourceCompact.setOnClickListener { + selected(it as ImageButton) + style = 1 + dialogBinding.layoutText.text = "Compact" + run = true + } + //Multi download + dialogBinding.downloadNo.text = "0" + dialogBinding.animeDownloadTop.setOnClickListener { + //Alert dialog asking for the number of chapters to download + val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup) + alertDialog.setTitle("Multi Chapter Downloader") + alertDialog.setMessage("Enter the number of chapters to download") + val input = NumberPicker(currContext()) + input.minValue = 1 + input.maxValue = 20 + input.value = 1 + alertDialog.setView(input) + alertDialog.setPositiveButton("OK") { dialog, which -> + dialogBinding.downloadNo.text = "${input.value}" + } + alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } + val dialog = alertDialog.show() + dialog.window?.setDimAmount(0.8f) + } + + //Scanlator + dialogBinding.animeScanlatorTop.setOnClickListener { + val dialogView2 = + LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) + val checkboxContainer = dialogView2.findViewById(R.id.checkboxContainer) + + // Dynamically add checkboxes + options.forEach { option -> + val checkBox = CheckBox(currContext()).apply { + text = option + } + //set checked if it's already selected + if (media.selected!!.scanlators != null) { + checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true + scanlatorSelectionListener?.onScanlatorsSelected() + } else { + checkBox.isChecked = true + } + checkboxContainer.addView(checkBox) + } + + // Create AlertDialog + val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup) + .setView(dialogView2) + .setPositiveButton("OK") { dialog, which -> + //add unchecked to hidden + hiddenScanlators.clear() + for (i in 0 until checkboxContainer.childCount) { + val checkBox = checkboxContainer.getChildAt(i) as CheckBox + if (!checkBox.isChecked) { + hiddenScanlators.add(checkBox.text.toString()) + } + } + fragment.onScanlatorChange(hiddenScanlators) + scanlatorSelectionListener?.onScanlatorsSelected() + } + .setNegativeButton("Cancel", null) + .show() + dialog.window?.setDimAmount(0.8f) + } + + nestedDialog = AlertDialog.Builder(fragment.requireContext() , R.style.MyPopup) + .setTitle("Options") + .setView(dialogView) + .setPositiveButton("OK") { _, _ -> + if(run) fragment.onIconPressed(style, reversed) + if (dialogBinding.downloadNo.text != "0"){ + fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt()) + } + } + .setNegativeButton("Cancel") { _, _ -> + } + .create() + nestedDialog?.show() + } //Chapter Handling handleChapters() } @@ -385,12 +422,15 @@ class MangaReadAdapter( parser.extension.sources.firstOrNull()?.lang ?: "Unknown" ) } - binding?.animeSourceLanguage?.setAdapter( - ArrayAdapter( - fragment.requireContext(), - R.layout.item_dropdown, - parser.extension.sources.map { it.lang }) + val adapter = ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) } ) + val items = adapter.count + if (items > 1) binding?.animeSourceLanguageContainer?.visibility = View.VISIBLE else binding?.animeSourceLanguageContainer?.visibility = View.GONE + + binding?.animeSourceLanguage?.setAdapter(adapter) } } diff --git a/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt b/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt index 25f8f6a4..48f6df75 100644 --- a/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt +++ b/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt @@ -24,7 +24,35 @@ class LanguageMapper { "vi" -> "Vietnamese" "zh" -> "Chinese" "zh-Hans" -> "Chinese (Simplified)" - else -> "" + "es-419" -> "Spanish (Latin America)" + "hu" -> "Hungarian" + "zh-habt" -> "Chinese (Hakka)" + "zh-hant" -> "Chinese (Traditional)" + "ca" -> "Catalan" + "bg" -> "Bulgarian" + "fa" -> "Persian" + "mn" -> "Mongolian" + "ro" -> "Romanian" + "he" -> "Hebrew" + "ms" -> "Malay" + "tl" -> "Tagalog" + "hi" -> "Hindi" + "my" -> "Burmese" + "cs" -> "Czech" + "pt" -> "Portuguese" + "nl" -> "Dutch" + "sv" -> "Swedish" + "bn" -> "Bengali" + "no" -> "Norwegian" + "el" -> "Greek" + "sr" -> "Serbian" + "da" -> "Danish" + "lt" -> "Lithuanian" + "ml" -> "Malayalam" + "mr" -> "Marathi" + "ta" -> "Tamil" + "te" -> "Telugu" + else -> code } } diff --git a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt index 1e38fc0e..75be1465 100644 --- a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt @@ -14,6 +14,7 @@ import ani.dantotsu.App.Companion.context import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.databinding.ActivityImageSearchBinding +import ani.dantotsu.initActivity import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.others.LangSet import ani.dantotsu.themes.ThemeManager @@ -49,6 +50,7 @@ class ImageSearchActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) + initActivity(this) ThemeManager(this).applyTheme() binding = ActivityImageSearchBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/res/drawable-v24/ic_banner_foreground.xml b/app/src/main/res/drawable-v24/ic_banner_foreground.xml deleted file mode 100644 index e26ad38f..00000000 --- a/app/src/main/res/drawable-v24/ic_banner_foreground.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_round_filter_24.xml b/app/src/main/res/drawable/ic_round_filter_24.xml new file mode 100644 index 00000000..c5b85484 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_filter_24.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/layout/activity_image_search.xml b/app/src/main/res/layout/activity_image_search.xml index 775ebf93..760a0b6e 100644 --- a/app/src/main/res/layout/activity_image_search.xml +++ b/app/src/main/res/layout/activity_image_search.xml @@ -2,8 +2,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="fill_parent" - android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:paddingTop="32dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_anime_watch.xml b/app/src/main/res/layout/item_anime_watch.xml index 70f66744..1834403a 100644 --- a/app/src/main/res/layout/item_anime_watch.xml +++ b/app/src/main/res/layout/item_anime_watch.xml @@ -90,12 +90,12 @@ @@ -107,7 +107,7 @@ - @@ -186,19 +177,26 @@ - + android:orientation="horizontal"> + + - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml b/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml index a0a0dece..cf3108b3 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-xhdpi/ic_banner.png b/app/src/main/res/mipmap-xhdpi/ic_banner.png index 8f8c3cad591ce5469d999f9f9cdf231ac6589944..89d36cbdbd69039406687ce34f17fb03167bb1b2 100644 GIT binary patch literal 21750 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 L`njxgN@xNA*Bd~A literal 5245 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 diff --git a/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..1bcf6a7a568dc1ad5c66b505c6d0edcd10ce06ae GIT binary patch literal 21750 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

ki1( zwmy>Dtjz6t$)$*2-B-{dDY%}2?QmklO2xSP!#|V~PD*Ut*IqUMLh-fhm#6kR*}6|m z+7fpD+|v&Qe1hse{)s_G$GukZ>C`=Y{V`3`^+2%Vy7vm9$EH1%i>U5-cVSKb3WuKA zXD(EkL^Axn_u+(%;-`%VFPyvhF4yE%EMI@$mE2{AnA^TgP&AX_y&LfG`IU{OpUq2* zgSs+epSI09Dg9W@cuQ=~M6a6>S4|{%+WJ5{9S;^9Nn*K>U?8*c#u{tTfu9K(ULPJN zWS%SjPz#z=XiHmYd32@Xtk0mM``DZ}U!3#kion@QK4s7epplt+g;^XR=D`b~v+5*x v+;u=xFoy-Z&;Bv~f5!gvdyl{0|1-XOzf;}1_;V8j0|SGntDnm{r-UW|Hxo)W literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/style.xml b/app/src/main/res/values/style.xml index 63ac05e8..e2c009ee 100644 --- a/app/src/main/res/values/style.xml +++ b/app/src/main/res/values/style.xml @@ -65,7 +65,7 @@ ?attr/colorOnBackground ?attr/colorSurface wrap_content - wrap_content + match_parent @drawable/shape_corner_16dp true true From 486be4827e2dfb55607b5e7f18e1f0e76b128e33 Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Sun, 31 Dec 2023 05:39:18 -0600 Subject: [PATCH 23/29] wrong download offset fix --- app/build.gradle | 2 +- .../main/java/ani/dantotsu/media/manga/MangaReadFragment.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 89adda86..5cc751b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ android { minSdk 23 targetSdk 34 versionCode ((System.currentTimeMillis() / 60000).toInteger()) - versionName "2.0.0-beta00-i" + versionName "2.0.0-beta00-iv2" signingConfig signingConfigs.debug } 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 241c979a..7e62e0f2 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -201,7 +201,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { val selected = media.userProgress val chapters = media.manga?.chapters?.values?.toList() //filter by selected language - val progressChapterIndex = chapters?.indexOfFirst { MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected } ?: 0 + val progressChapterIndex = (chapters?.indexOfFirst { MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected } ?: 0) + 1 if (progressChapterIndex < 0 || n < 1 || chapters == null) return @@ -209,7 +209,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { val endIndex = minOf(progressChapterIndex + n, chapters.size) //make sure there are enough chapters - val chaptersToDownload = chapters.subList(progressChapterIndex + 1, endIndex) + val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex) + for (chapter in chaptersToDownload) { onMangaChapterDownloadClick(chapter.title!!) From ae8b952b4c9ee9c61da93cbd882be4e8151141bb Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Sun, 31 Dec 2023 05:45:27 -0600 Subject: [PATCH 24/29] floating nav bar?? --- app/src/main/res/layout/activity_media.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_media.xml b/app/src/main/res/layout/activity_media.xml index a3411728..c932fc0a 100644 --- a/app/src/main/res/layout/activity_media.xml +++ b/app/src/main/res/layout/activity_media.xml @@ -242,7 +242,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="0dp" - android:layout_gravity="bottom" + android:layout_gravity="center_horizontal|bottom" android:background="?attr/colorSurface" android:translationZ="1dp" app:itemActiveIndicatorStyle="@style/BottomNavBar" From ef30869b62cf8531e64ac6054f6df32686b61fe8 Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Sun, 31 Dec 2023 06:18:55 -0600 Subject: [PATCH 25/29] more visible nav --- app/src/main/java/ani/dantotsu/MainActivity.kt | 8 +------- app/src/main/res/drawable/bottom_nav.xml | 2 +- app/src/main/res/layout/item_navbar.xml | 5 ++--- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index 50d681c5..3b8c4bba 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -79,16 +79,10 @@ class MainActivity : AppCompatActivity() { val backgroundDrawable = _bottomBar.background as GradientDrawable val currentColor = backgroundDrawable.color?.defaultColor ?: 0 - val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt() + val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF0000000.toInt() backgroundDrawable.setColor(semiTransparentColor) _bottomBar.background = backgroundDrawable } - val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) - .getBoolean("colorOverflow", false) - if (!colorOverflow) { - _bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray) - - } var doubleBackToExitPressedOnce = false diff --git a/app/src/main/res/drawable/bottom_nav.xml b/app/src/main/res/drawable/bottom_nav.xml index ba236777..b42dd3ce 100644 --- a/app/src/main/res/drawable/bottom_nav.xml +++ b/app/src/main/res/drawable/bottom_nav.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_navbar.xml b/app/src/main/res/layout/item_navbar.xml index 417d4e10..0117ac70 100644 --- a/app/src/main/res/layout/item_navbar.xml +++ b/app/src/main/res/layout/item_navbar.xml @@ -15,9 +15,8 @@ android:layout_marginTop="16dp" android:layout_marginBottom="32dp" android:background="@drawable/bottom_nav" - android:elevation="4dp" - android:padding="8dp" - android:translationZ="12dp" + android:elevation="8dp" + android:padding="6dp" android:visibility="gone" app:abb_animationDuration="300" app:abb_animationInterpolator="@anim/over_shoot" From 4286232d17d79e79ff4af7015219776a6f035d70 Mon Sep 17 00:00:00 2001 From: aayush262 <99584765+aayush2622@users.noreply.github.com> Date: Thu, 4 Jan 2024 21:22:07 +0530 Subject: [PATCH 26/29] Language name in extension setting (#111) * Full language name in ext settings * added more lang name * changed alter dialog view * sort language by names * 3x grid for 360DP mobiles * Default novel settings * Oled for LN * Lang full name * Notification icon changed to dantotsu * Remember 'sort' value --- .../connections/anilist/AnilistQueries.kt | 6 +- .../java/ani/dantotsu/media/SearchActivity.kt | 2 +- .../dantotsu/media/anime/AnimeWatchAdapter.kt | 2 +- .../media/anime/AnimeWatchFragment.kt | 19 +- .../dantotsu/media/manga/MangaReadAdapter.kt | 2 +- .../dantotsu/media/manga/MangaReadFragment.kt | 19 +- .../novel/novelreader/NovelReaderActivity.kt | 40 +- .../NovelReaderSettingsDialogFragment.kt | 23 +- .../ani/dantotsu/media/user/ListActivity.kt | 6 +- .../ani/dantotsu/media/user/ListFragment.kt | 2 +- .../ani/dantotsu/others/LanguageMapper.kt | 127 +- .../settings/CurrentNovelReaderSettings.kt | 1 + .../InstalledAnimeExtensionsFragment.kt | 2 +- .../InstalledMangaExtensionsFragment.kt | 2 +- .../dantotsu/settings/NovelReaderSettings.kt | 10 - .../ani/dantotsu/settings/ReaderSettings.kt | 1 + .../settings/ReaderSettingsActivity.kt | 173 +- app/src/main/res/drawable/monochrome.xml | 18 +- .../res/layout/activity_reader_settings.xml | 1661 +++++++++++------ ...om_sheet_current_novel_reader_settings.xml | 19 + app/src/main/res/layout/item_anime_watch.xml | 1 + 21 files changed, 1487 insertions(+), 649 deletions(-) delete mode 100644 app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt index a0c1c270..2302a3d6 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -1,6 +1,7 @@ package ani.dantotsu.connections.anilist import android.app.Activity +import android.content.Context import ani.dantotsu.R import ani.dantotsu.checkGenreTime import ani.dantotsu.checkId @@ -410,8 +411,9 @@ class AnilistQueries { sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder }) sorted["All"] = all - - val sort = sortOrder ?: options?.rowOrder + val listsort = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + ?.getString("sort_order", "score") + val sort = listsort ?: sortOrder ?: options?.rowOrder for (i in sorted.keys) { when (sort) { "score" -> sorted[i]?.sortWith { b, a -> diff --git a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt index b3cf36f5..0357ea5f 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt @@ -78,7 +78,7 @@ class SearchActivity : AppCompatActivity() { mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true) val headerAdaptor = SearchAdapter(this) - val gridSize = (screenWidth / 124f).toInt() + val gridSize = (screenWidth / 120f).toInt() val gridLayoutManager = GridLayoutManager(this, gridSize) gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { 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 c6a2240a..2c4244ab 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -395,7 +395,7 @@ class AnimeWatchAdapter( val adapter = ArrayAdapter( fragment.requireContext(), R.layout.item_dropdown, - parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) } + parser.extension.sources.sortedBy { it.lang }.map { LanguageMapper.mapLanguageCodeToName(it.lang) } ) val items = adapter.count if (items > 1) binding?.animeSourceLanguageContainer?.visibility = View.VISIBLE else binding?.animeSourceLanguageContainer?.visibility = View.GONE 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 5dba0e85..5fc6635f 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -23,6 +23,7 @@ import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsViewModel +import ani.dantotsu.others.LanguageMapper import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.HAnimeSources @@ -314,19 +315,19 @@ class AnimeWatchFragment : Fragment() { if (show) View.GONE else View.VISIBLE } } + var itemSelected = false val allSettings = pkg.sources.filterIsInstance() if (allSettings.isNotEmpty()) { var selectedSetting = allSettings[0] if (allSettings.size > 1) { - val names = allSettings.map { it.lang }.toTypedArray() + val names = allSettings.sortedBy { it.lang }.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray() var selectedIndex = 0 - val dialog = AlertDialog.Builder(requireContext()) + val dialog = AlertDialog.Builder(requireContext() , R.style.MyPopup) .setTitle("Select a Source") - .setSingleChoiceItems(names, selectedIndex) { _, which -> + .setSingleChoiceItems(names, selectedIndex) { dialog, which -> selectedIndex = which - } - .setPositiveButton("OK") { dialog, _ -> selectedSetting = allSettings[selectedIndex] + itemSelected = true dialog.dismiss() // Move the fragment transaction here @@ -343,10 +344,10 @@ class AnimeWatchFragment : Fragment() { .commit() } } - .setNegativeButton("Cancel") { dialog, _ -> - dialog.cancel() - changeUIVisibility(true) - return@setNegativeButton + .setOnDismissListener { + if (!itemSelected) { + changeUIVisibility(true) + } } .show() dialog.window?.setDimAmount(0.8f) 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 8aa03489..9237c6b9 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt @@ -425,7 +425,7 @@ class MangaReadAdapter( val adapter = ArrayAdapter( fragment.requireContext(), R.layout.item_dropdown, - parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) } + parser.extension.sources.sortedBy { it.lang }.map { LanguageMapper.mapLanguageCodeToName(it.lang) } ) val items = adapter.count if (items > 1) binding?.animeSourceLanguageContainer?.visibility = View.VISIBLE else binding?.animeSourceLanguageContainer?.visibility = View.GONE 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 7e62e0f2..51d1c806 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -37,6 +37,7 @@ import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog +import ani.dantotsu.others.LanguageMapper import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.MangaParser @@ -357,19 +358,19 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { if (show) View.GONE else View.VISIBLE } } + var itemSelected = false val allSettings = pkg.sources.filterIsInstance() if (allSettings.isNotEmpty()) { var selectedSetting = allSettings[0] if (allSettings.size > 1) { - val names = allSettings.map { it.lang }.toTypedArray() + val names = allSettings.sortedBy { it.lang }.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray() var selectedIndex = 0 - val dialog = AlertDialog.Builder(requireContext()) + val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) .setTitle("Select a Source") - .setSingleChoiceItems(names, selectedIndex) { _, which -> + .setSingleChoiceItems(names, selectedIndex) { dialog, which -> selectedIndex = which - } - .setPositiveButton("OK") { dialog, _ -> selectedSetting = allSettings[selectedIndex] + itemSelected = true dialog.dismiss() // Move the fragment transaction here @@ -384,10 +385,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { .addToBackStack(null) .commit() } - .setNegativeButton("Cancel") { dialog, _ -> - dialog.cancel() - changeUIVisibility(true) - return@setNegativeButton + .setOnDismissListener { + if (!itemSelected) { + changeUIVisibility(true) + } } .show() dialog.window?.setDimAmount(0.8f) diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt index 069fef28..f76e6ff5 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt @@ -36,7 +36,7 @@ import ani.dantotsu.saveData import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.settings.CurrentNovelReaderSettings import ani.dantotsu.settings.CurrentReaderSettings -import ani.dantotsu.settings.NovelReaderSettings +import ani.dantotsu.settings.ReaderSettings import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.snackString import ani.dantotsu.themes.ThemeManager @@ -62,7 +62,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { private lateinit var binding: ActivityNovelReaderBinding private val scope = lifecycleScope - lateinit var settings: NovelReaderSettings + lateinit var settings: ReaderSettings private lateinit var uiSettings: UserInterfaceSettings private var notchHeight: Int? = null @@ -159,9 +159,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { ThemeManager(this).applyTheme() binding = ActivityNovelReaderBinding.inflate(layoutInflater) setContentView(binding.root) - - settings = loadData("novel_reader_settings", this) - ?: NovelReaderSettings().apply { saveData("novel_reader_settings", this) } + settings = loadData("reader_settings", this) + ?: ReaderSettings().apply { saveData("reader_settings", this) } uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().also { saveData("ui_settings", it) } @@ -271,7 +270,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { binding.bookReader.getAppearance { currentTheme = it themes.add(0, it) - settings.default = loadData("${sanitizedBookId}_current_settings") ?: settings.default + settings.defaultLN = loadData("${sanitizedBookId}_current_settings") ?: settings.defaultLN applySettings() } @@ -323,7 +322,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { return when (event.keyCode) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> { if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP) - if (!settings.default.volumeButtons) + if (!settings.defaultLN.volumeButtons) return false if (event.action == KeyEvent.ACTION_DOWN) { onVolumeUp?.invoke() @@ -333,7 +332,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> { if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) - if (!settings.default.volumeButtons) + if (!settings.defaultLN.volumeButtons) return false if (event.action == KeyEvent.ACTION_DOWN) { onVolumeDown?.invoke() @@ -349,13 +348,18 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { fun applySettings() { - saveData("${sanitizedBookId}_current_settings", settings.default) + saveData("${sanitizedBookId}_current_settings", settings.defaultLN) hideBars() + if(settings.defaultLN.useOledTheme) { + themes.forEach { theme -> + theme.darkBg = Color.parseColor("#000000") + } + } currentTheme = - themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) } + themes.first { it.name.equals(settings.defaultLN.currentThemeName, ignoreCase = true) } - when (settings.default.layout) { + when (settings.defaultLN.layout) { CurrentNovelReaderSettings.Layouts.PAGED -> { currentTheme?.flow = ReaderFlow.PAGINATED } @@ -366,22 +370,22 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { } requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER - when (settings.default.dualPageMode) { + when (settings.defaultLN.dualPageMode) { CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1 CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2 CurrentReaderSettings.DualPageModes.Force -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE } - currentTheme?.lineHeight = settings.default.lineHeight - currentTheme?.gap = settings.default.margin - currentTheme?.maxInlineSize = settings.default.maxInlineSize - currentTheme?.maxBlockSize = settings.default.maxBlockSize - currentTheme?.useDark = settings.default.useDarkTheme + currentTheme?.lineHeight = settings.defaultLN.lineHeight + currentTheme?.gap = settings.defaultLN.margin + currentTheme?.maxInlineSize = settings.defaultLN.maxInlineSize + currentTheme?.maxBlockSize = settings.defaultLN.maxBlockSize + currentTheme?.useDark = settings.defaultLN.useDarkTheme currentTheme?.let { binding.bookReader.setAppearance(it) } - if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + if (settings.defaultLN.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt index 27a9caaa..760c7eb3 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt @@ -30,8 +30,7 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val activity = requireActivity() as NovelReaderActivity - val settings = activity.settings.default - + val settings = activity.settings.defaultLN val themeLabels = activity.themes.map { it.name } binding.themeSelect.adapter = NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels) @@ -49,7 +48,11 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { override fun onNothingSelected(parent: AdapterView<*>?) {} } - + binding.useOledTheme.isChecked = settings.useOledTheme + binding.useOledTheme.setOnCheckedChangeListener { _, isChecked -> + settings.useOledTheme = isChecked + activity.applySettings() + } val layoutList = listOf( binding.paged, binding.continuous @@ -173,6 +176,20 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { binding.maxBlockSize.setText(value.toString()) activity.applySettings() } + + } + binding.incrementMaxBlockSize.setOnClickListener { + val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720 + settings.maxBlockSize = value + 10 + binding.maxBlockSize.setText(settings.maxBlockSize.toString()) + activity.applySettings() + } + + binding.decrementMaxBlockSize.setOnClickListener { + val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720 + settings.maxBlockSize = value - 10 + binding.maxBlockSize.setText(settings.maxBlockSize.toString()) + activity.applySettings() } binding.useDarkTheme.isChecked = settings.useDarkTheme diff --git a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt index 291213c1..87930156 100644 --- a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt @@ -1,6 +1,7 @@ package ani.dantotsu.media.user import android.annotation.SuppressLint +import android.content.Context import android.os.Bundle import android.util.TypedValue import android.view.View @@ -14,9 +15,11 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope import ani.dantotsu.R import ani.dantotsu.Refresh +import ani.dantotsu.currContext import ani.dantotsu.databinding.ActivityListBinding import ani.dantotsu.loadData import ani.dantotsu.others.LangSet +import ani.dantotsu.saveData import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.themes.ThemeManager import com.google.android.material.tabs.TabLayout @@ -144,7 +147,8 @@ class ListActivity : AppCompatActivity() { R.id.release -> "release" else -> null } - + currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit() + ?.putString("sort_order", sort)?.apply() binding.listProgressBar.visibility = View.VISIBLE binding.listViewPager.adapter = null scope.launch { diff --git a/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt b/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt index 3eccf001..48573e50 100644 --- a/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt @@ -46,7 +46,7 @@ class ListFragment : Fragment() { binding.listRecyclerView.layoutManager = GridLayoutManager( requireContext(), - if (grid!!) (screenWidth / 124f).toInt() else 1 + if (grid!!) (screenWidth / 120f).toInt() else 1 ) binding.listRecyclerView.adapter = adapter } diff --git a/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt b/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt index 48f6df75..3d9c439b 100644 --- a/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt +++ b/app/src/main/java/ani/dantotsu/others/LanguageMapper.kt @@ -6,52 +6,115 @@ class LanguageMapper { fun mapLanguageCodeToName(code: String): String { return when (code) { "all" -> "Multi" + "af" -> "Afrikaans" + "am" -> "Amharic" "ar" -> "Arabic" + "as" -> "Assamese" + "az" -> "Azerbaijani" + "be" -> "Belarusian" + "bg" -> "Bulgarian" + "bn" -> "Bengali" + "bs" -> "Bosnian" + "ca" -> "Catalan" + "ceb" -> "Cebuano" + "cs" -> "Czech" + "da" -> "Danish" "de" -> "German" + "el" -> "Greek" "en" -> "English" + "en-Us" -> "English (United States)" + "eo" -> "Esperanto" "es" -> "Spanish" + "es-419" -> "Spanish (Latin America)" + "et" -> "Estonian" + "eu" -> "Basque" + "fa" -> "Persian" + "fi" -> "Finnish" + "fil" -> "Filipino" + "fo" -> "Faroese" "fr" -> "French" + "ga" -> "Irish" + "gn" -> "Guarani" + "gu" -> "Gujarati" + "ha" -> "Hausa" + "he" -> "Hebrew" + "hi" -> "Hindi" + "hr" -> "Croatian" + "ht" -> "Haitian Creole" + "hu" -> "Hungarian" + "hy" -> "Armenian" "id" -> "Indonesian" + "ig" -> "Igbo" + "is" -> "Icelandic" "it" -> "Italian" "ja" -> "Japanese" + "jv" -> "Javanese" + "ka" -> "Georgian" + "kk" -> "Kazakh" + "km" -> "Khmer" + "kn" -> "Kannada" "ko" -> "Korean" - "pl" -> "Polish" - "pt-BR" -> "Portuguese (Brazil)" - "ru" -> "Russian" - "th" -> "Thai" - "tr" -> "Turkish" - "uk" -> "Ukrainian" - "vi" -> "Vietnamese" - "zh" -> "Chinese" - "zh-Hans" -> "Chinese (Simplified)" - "es-419" -> "Spanish (Latin America)" - "hu" -> "Hungarian" - "zh-habt" -> "Chinese (Hakka)" - "zh-hant" -> "Chinese (Traditional)" - "ca" -> "Catalan" - "bg" -> "Bulgarian" - "fa" -> "Persian" - "mn" -> "Mongolian" - "ro" -> "Romanian" - "he" -> "Hebrew" - "ms" -> "Malay" - "tl" -> "Tagalog" - "hi" -> "Hindi" - "my" -> "Burmese" - "cs" -> "Czech" - "pt" -> "Portuguese" - "nl" -> "Dutch" - "sv" -> "Swedish" - "bn" -> "Bengali" - "no" -> "Norwegian" - "el" -> "Greek" - "sr" -> "Serbian" - "da" -> "Danish" + "ku" -> "Kurdish" + "ky" -> "Kyrgyz" + "la" -> "Latin" + "lb" -> "Luxembourgish" + "lo" -> "Lao" "lt" -> "Lithuanian" + "lv" -> "Latvian" + "mg" -> "Malagasy" + "mi" -> "Maori" + "mk" -> "Macedonian" "ml" -> "Malayalam" + "mn" -> "Mongolian" + "mo" -> "Moldovan" "mr" -> "Marathi" + "ms" -> "Malay" + "mt" -> "Maltese" + "my" -> "Burmese" + "ne" -> "Nepali" + "nl" -> "Dutch" + "no" -> "Norwegian" + "ny" -> "Chichewa" + "pl" -> "Polish" + "pt" -> "Portuguese" + "pt-BR" -> "Portuguese (Brazil)" + "pt-PT" -> "Portuguese (Portugal)" + "ps" -> "Pashto" + "ro" -> "Romanian" + "rm" -> "Romansh" + "ru" -> "Russian" + "sd" -> "Sindhi" + "sh" -> "Serbo-Croatian" + "si" -> "Sinhala" + "sk" -> "Slovak" + "sl" -> "Slovenian" + "sm" -> "Samoan" + "sn" -> "Shona" + "so" -> "Somali" + "sq" -> "Albanian" + "sr" -> "Serbian" + "st" -> "Southern Sotho" + "sv" -> "Swedish" + "sw" -> "Swahili" "ta" -> "Tamil" "te" -> "Telugu" + "tg" -> "Tajik" + "th" -> "Thai" + "ti" -> "Tigrinya" + "tk" -> "Turkmen" + "tl" -> "Tagalog" + "to" -> "Tongan" + "tr" -> "Turkish" + "uk" -> "Ukrainian" + "ur" -> "Urdu" + "uz" -> "Uzbek" + "vi" -> "Vietnamese" + "yo" -> "Yoruba" + "zh" -> "Chinese" + "zh-Hans" -> "Chinese (Simplified)" + "zh-Hant" -> "Chinese (Traditional)" + "zh-Habt" -> "Chinese (Hakka)" + "zu" -> "Zulu" else -> code } } diff --git a/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt index bbe3fcd3..b1cd429b 100644 --- a/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt +++ b/app/src/main/java/ani/dantotsu/settings/CurrentNovelReaderSettings.kt @@ -11,6 +11,7 @@ data class CurrentNovelReaderSettings( var justify: Boolean = true, var hyphenation: Boolean = true, var useDarkTheme: Boolean = false, + var useOledTheme: Boolean = false, var invert: Boolean = false, var maxInlineSize: Int = 720, var maxBlockSize: Int = 1440, diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt index 49489825..d921b687 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt @@ -67,7 +67,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { if (allSettings.isNotEmpty()) { var selectedSetting = allSettings[0] if (allSettings.size > 1) { - val names = allSettings.map { it.lang }.toTypedArray() + val names = allSettings.sortedBy { it.lang }.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray() var selectedIndex = 0 val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) .setTitle("Select a Source") diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt index f31319d9..79db1197 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt @@ -64,7 +64,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { if (allSettings.isNotEmpty()) { var selectedSetting = allSettings[0] if (allSettings.size > 1) { - val names = allSettings.map { it.lang }.toTypedArray() + val names = allSettings.sortedBy { it.lang }.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray() var selectedIndex = 0 val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) .setTitle("Select a Source") diff --git a/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt deleted file mode 100644 index 7c72c7e9..00000000 --- a/app/src/main/java/ani/dantotsu/settings/NovelReaderSettings.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ani.dantotsu.settings - -import java.io.Serializable - -data class NovelReaderSettings( - var showSource: Boolean = true, - var showSystemBars: Boolean = false, - var default: CurrentNovelReaderSettings = CurrentNovelReaderSettings(), - var askIndividual: Boolean = true, -) : Serializable \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt b/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt index 8a249dd7..e0a91af8 100644 --- a/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt +++ b/app/src/main/java/ani/dantotsu/settings/ReaderSettings.kt @@ -8,6 +8,7 @@ data class ReaderSettings( var autoDetectWebtoon: Boolean = true, var default: CurrentReaderSettings = CurrentReaderSettings(), + var defaultLN: CurrentNovelReaderSettings = CurrentNovelReaderSettings(), var askIndividual: Boolean = true, var updateForH: Boolean = false diff --git a/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt index 61c87afe..67bd04d7 100644 --- a/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt @@ -1,13 +1,17 @@ package ani.dantotsu.settings import android.os.Bundle +import android.view.View import android.view.ViewGroup +import android.widget.AdapterView import androidx.appcompat.app.AppCompatActivity import androidx.core.view.updateLayoutParams +import ani.dantotsu.NoPaddingArrayAdapter import ani.dantotsu.R import ani.dantotsu.databinding.ActivityReaderSettingsBinding import ani.dantotsu.initActivity import ani.dantotsu.loadData +import ani.dantotsu.media.novel.novelreader.NovelReaderActivity import ani.dantotsu.navBarHeight import ani.dantotsu.others.LangSet import ani.dantotsu.saveData @@ -42,7 +46,7 @@ class ReaderSettingsActivity : AppCompatActivity() { onBackPressedDispatcher.onBackPressed() } - //General + //Manga Settings binding.readerSettingsSourceName.isChecked = settings.showSource binding.readerSettingsSourceName.setOnCheckedChangeListener { _, isChecked -> settings.showSource = isChecked @@ -54,14 +58,14 @@ class ReaderSettingsActivity : AppCompatActivity() { settings.showSystemBars = isChecked saveData(reader, settings) } - + //Default Manga binding.readerSettingsAutoWebToon.isChecked = settings.autoDetectWebtoon binding.readerSettingsAutoWebToon.setOnCheckedChangeListener { _, isChecked -> settings.autoDetectWebtoon = isChecked saveData(reader, settings) } - //Default + val layoutList = listOf( binding.readerSettingsPaged, binding.readerSettingsContinuousPaged, @@ -185,6 +189,169 @@ class ReaderSettingsActivity : AppCompatActivity() { saveData(reader, settings) } + //LN settings + val layoutListLN = listOf( + binding.LNpaged, + binding.LNcontinuous + ) + + binding.LNlayoutText.text = settings.defaultLN.layout.string + var selectedLN = layoutListLN[settings.defaultLN.layout.ordinal] + selectedLN.alpha = 1f + + layoutListLN.forEachIndexed { index, imageButton -> + imageButton.setOnClickListener { + selectedLN.alpha = 0.33f + selectedLN = imageButton + selectedLN.alpha = 1f + settings.defaultLN.layout = CurrentNovelReaderSettings.Layouts[index] + ?: CurrentNovelReaderSettings.Layouts.PAGED + binding.LNlayoutText.text = settings.defaultLN.layout.string + saveData(reader, settings) + } + } + + val dualListLN = listOf( + binding.LNdualNo, + binding.LNdualAuto, + binding.LNdualForce + ) + + binding.LNdualPageText.text = settings.defaultLN.dualPageMode.toString() + var selectedDualLN = dualListLN[settings.defaultLN.dualPageMode.ordinal] + selectedDualLN.alpha = 1f + + dualListLN.forEachIndexed { index, imageButton -> + imageButton.setOnClickListener { + selectedDualLN.alpha = 0.33f + selectedDualLN = imageButton + selectedDualLN.alpha = 1f + settings.defaultLN.dualPageMode = CurrentReaderSettings.DualPageModes[index] + ?: CurrentReaderSettings.DualPageModes.Automatic + binding.LNdualPageText.text = settings.defaultLN.dualPageMode.toString() + saveData(reader, settings) + } + } + + binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString()) + binding.LNlineHeight.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f + settings.defaultLN.lineHeight = value + binding.LNlineHeight.setText(value.toString()) + saveData(reader, settings) + } + } + + binding.LNincrementLineHeight.setOnClickListener { + val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f + settings.defaultLN.lineHeight = value + 0.1f + binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString()) + saveData(reader, settings) + } + + binding.LNdecrementLineHeight.setOnClickListener { + val value = binding.LNlineHeight.text.toString().toFloatOrNull() ?: 1.4f + settings.defaultLN.lineHeight = value - 0.1f + binding.LNlineHeight.setText(settings.defaultLN.lineHeight.toString()) + saveData(reader, settings) + } + + binding.LNmargin.setText(settings.defaultLN.margin.toString()) + binding.LNmargin.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f + settings.defaultLN.margin = value + binding.LNmargin.setText(value.toString()) + saveData(reader, settings) + } + } + + binding.LNincrementMargin.setOnClickListener { + val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f + settings.defaultLN.margin = value + 0.01f + binding.LNmargin.setText(settings.defaultLN.margin.toString()) + saveData(reader, settings) + } + + binding.LNdecrementMargin.setOnClickListener { + val value = binding.LNmargin.text.toString().toFloatOrNull() ?: 0.06f + settings.defaultLN.margin = value - 0.01f + binding.LNmargin.setText(settings.defaultLN.margin.toString()) + saveData(reader, settings) + } + + binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString()) + binding.LNmaxInlineSize.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720 + settings.defaultLN.maxInlineSize = value + binding.LNmaxInlineSize.setText(value.toString()) + saveData(reader, settings) + } + } + + binding.LNincrementMaxInlineSize.setOnClickListener { + val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720 + settings.defaultLN.maxInlineSize = value + 10 + binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString()) + saveData(reader, settings) + } + + binding.LNdecrementMaxInlineSize.setOnClickListener { + val value = binding.LNmaxInlineSize.text.toString().toIntOrNull() ?: 720 + settings.defaultLN.maxInlineSize = value - 10 + binding.LNmaxInlineSize.setText(settings.defaultLN.maxInlineSize.toString()) + saveData(reader, settings) + } + + binding.LNmaxBlockSize.setText(settings.defaultLN.maxBlockSize.toString()) + binding.LNmaxBlockSize.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720 + settings.defaultLN.maxBlockSize = value + binding.LNmaxBlockSize.setText(value.toString()) + saveData(reader, settings) + } + } + binding.LNincrementMaxBlockSize.setOnClickListener { + val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720 + settings.defaultLN.maxInlineSize = value + 10 + binding.LNmaxBlockSize.setText(settings.defaultLN.maxInlineSize.toString()) + saveData(reader, settings) + } + + binding.LNdecrementMaxBlockSize.setOnClickListener { + val value = binding.LNmaxBlockSize.text.toString().toIntOrNull() ?: 720 + settings.defaultLN.maxBlockSize = value - 10 + binding.LNmaxBlockSize.setText(settings.defaultLN.maxBlockSize.toString()) + saveData(reader, settings) + } + + binding.LNuseDarkTheme.isChecked = settings.defaultLN.useDarkTheme + binding.LNuseDarkTheme.setOnCheckedChangeListener { _, isChecked -> + settings.defaultLN.useDarkTheme = isChecked + saveData(reader, settings) + } + + binding.LNuseOledTheme.isChecked = settings.defaultLN.useOledTheme + binding.LNuseOledTheme.setOnCheckedChangeListener { _, isChecked -> + settings.defaultLN.useOledTheme = isChecked + saveData(reader, settings) + } + + binding.LNkeepScreenOn.isChecked = settings.defaultLN.keepScreenOn + binding.LNkeepScreenOn.setOnCheckedChangeListener { _, isChecked -> + settings.defaultLN.keepScreenOn = isChecked + saveData(reader, settings) + } + + binding.LNvolumeButton.isChecked = settings.defaultLN.volumeButtons + binding.LNvolumeButton.setOnCheckedChangeListener { _, isChecked -> + settings.defaultLN.volumeButtons = isChecked + saveData(reader, settings) + } + //Update Progress binding.readerSettingsAskUpdateProgress.isChecked = settings.askIndividual binding.readerSettingsAskUpdateProgress.setOnCheckedChangeListener { _, isChecked -> diff --git a/app/src/main/res/drawable/monochrome.xml b/app/src/main/res/drawable/monochrome.xml index 7b1f2b2d..6fd3cc32 100644 --- a/app/src/main/res/drawable/monochrome.xml +++ b/app/src/main/res/drawable/monochrome.xml @@ -1,16 +1,16 @@ + + android:pathData="M44.26,128C44.26,173.48 80.53,210.4 125.71,211.63L125.71,128.01L642.29,128.01L642.29,639.97L768,639.97L768,128L44.26,128zM642.29,639.97L125.71,639.97L125.71,639.99L642.29,639.99L642.29,639.97zM125.71,639.97L125.71,556.38C80.54,557.6 44.28,594.5 44.26,639.97L125.71,639.97zM125.71,556.38C126.48,556.35 127.23,556.26 128,556.26L384,556.26C479.14,556.26 556.26,479.13 556.26,384C556.26,288.86 479.13,211.74 384,211.74L128,211.74C127.23,211.74 126.48,211.65 125.71,211.63L125.71,286.18L384,286.18C438.02,286.18 481.82,329.98 481.82,384C481.82,438.03 438.02,481.82 384,481.82L125.71,481.82L125.71,556.38zM125.71,481.82L125.71,286.18L0,286.18L0,481.82L125.71,481.82z"/> - + android:pathData="m442,366.7l-76.02,-43.89c-13.32,-7.69 -29.96,1.92 -29.96,17.3v87.78c0,15.38 16.65,24.99 29.96,17.3l76.02,-43.89c13.32,-7.69 13.32,-26.91 0,-34.6Z"/> diff --git a/app/src/main/res/layout/activity_reader_settings.xml b/app/src/main/res/layout/activity_reader_settings.xml index 0600341a..e8886066 100644 --- a/app/src/main/res/layout/activity_reader_settings.xml +++ b/app/src/main/res/layout/activity_reader_settings.xml @@ -73,6 +73,1119 @@ android:clipToPadding="false" android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -130,553 +1244,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Date: Thu, 4 Jan 2024 11:17:49 -0600 Subject: [PATCH 27/29] debuggable false --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5cc751b6..59735059 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { debug { applicationIdSuffix ".beta" manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"] - debuggable true + debuggable false } release { manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"] From 51b3aac0c08ad38a3b03488c1f81640253e2b7b8 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Thu, 4 Jan 2024 11:28:27 -0600 Subject: [PATCH 28/29] Update strings.xml --- app/src/main/res/values-en-rDW/strings.xml | 647 --------------------- 1 file changed, 647 deletions(-) diff --git a/app/src/main/res/values-en-rDW/strings.xml b/app/src/main/res/values-en-rDW/strings.xml index 293ce3fc..e69de29b 100644 --- a/app/src/main/res/values-en-rDW/strings.xml +++ b/app/src/main/res/values-en-rDW/strings.xml @@ -1,647 +0,0 @@ - - - rebelonion/Dantotsu - dantotsuprefs - - Dantotsu - The NEW Best Anime & Manga app for Android. - - Login - Logout - - https://discord.gg/4HPZ5nAWwM - https://github.com/rebelonion/Dantotsu - - Home - Anime - Browse Anime - Manga - Browse Manga - Info - Watch - Read - - Anime List - Manga List - - X( - :o - " : " - - No Internet Connection - Refresh - - Search - Search Results - Sort By - Genre - Top Score - Recently Updated - Trending Anime - Popular Anime - Trending Manga - Trending Novel - Popular Manga - - Username - "Chapters Read " - "Episodes Watched " - Continue Reading - Continue Watching - Recommended - - Watch/Read some Anime or Manga to get Recommendations - All caught up, when New? - Settings - - Add to List - List Editor - Add to Favourites - Notifications - - Reading - Watching - Completed - Paused - Dropped - Planning - Favourites - Rewatching - Rereading - All - - - STATUS - - PLANNING - CURRENT - COMPLETED - REPEATING - PAUSED - DROPPED - - - PLANNING - WATCHING - COMPLETED - RE-WATCHING - PAUSED - DROPPED - - - PLANNING - READING - COMPLETED - RE-READING - PAUSED - DROPPED - - PROGRESS - SCORE - " / 10" - STARTED AT - COMPLETED AT - Save - Delete - Remove - - Name - Name Romaji - Mean Score - Format - Status - Total Episodes - Total Chapters - Average Duration - " min" - Season - Start Date - End Date - Source - Studio - Genres - Synopsis - Characters - Relations - - Roles - Details - - Play on Youtube - Episodes - Episode - Chapters - Chapter - Wrong Title? - - Couldn\'t find anything X( \n - Try another source. - - %1$s is not supported! - Select Server - Auto Selecting Server - Make Default - Filler - Adult - List Only - Tag - Tags - Synonyms - Trailer - Opening - Ending - Prequel - Sequel - - Anilist Settings - Extensions - Downloads - Settings - Extensions - Player Settings - Only show My Shows in Recently Updated - Download Manager - Download in SD card - No SD card was Found. - Reader Settings - Default Source - Show Youtube Link - Default Episode Layout - Default Chapter Layout - User Interface - Common - Theme - UI Settings - About - " Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-of-the-art elegance. It is an Anilist only client, which also lets you stream-download Anime through extensions & Manga.\nDantotsu literally means the \"best of the best\" in japanese. Well, we would like to say this is the best open source app for anime and manga on Android, what would you say?" - Developers - Disclaimer - - - Dantotsu by itself only provides an anime and manga tracker and does not provide any anime or manga streaming or downloading capabilities. - \n\n - Dantotsu or any of its developer/staff don\'t host any of the content found inside Dantotsu. Any and all images and anime/manga information found in the app are taken from various public APIs (AniList, MyAnimeList, Kitsu). - \n\n - Furthermore, all of the anime/manga links found in Dantotsu are taken from various 3rd party plugins and have no affiliation with Dantotsu or its staff. - \n\n - Dantotsu or it\'s owners aren\'t liable for any misuse of any of the contents found inside or outside of the app and cannot be held accountable for the distribution of any of the contents found inside the app. - \n\n - By using Dantotsu, you comply to the fact that the developer of the app is not responsible for any of the contents found in the app. You also agree to the fact that you may not use Dantotsu to download or stream any copyrighted content. - \n\n - If the internet infringement issues are involved, please contact the source website. The developer does not assume any legal responsibility. - - Version %1$s - - You can Long Click an episode/chapter to Mark it as Read. - Long Clicking Shows can directly open List Editor. - There are few more easter eggs hidden in the App. - Challenge: Go to the very bottom of the Popular Anime & Manga - Try Long Clicking the Show\'s Title. - Damn, why are you wasting your Time? - You can Long Click to copy this Message. - OMG LOOK! YOU FOUND AN EASTER EGG!? - You know who else likes this Animation? - MAL support? bruh. - Novels? more like NO vels. - Long Click the logo to check for App Update - - - - Default - Theme 1 - Theme 2 - Theme 3 - Theme 4 - - - Video - Show Video Info - Show Source Name - Shows what the Resolution of the current video playing, useful for \"Multi Quality\" servers. - Auto Quality Selection - Height - Width - Automatically uses the closest quality provided by default, ONLY applied for \"Multi Quality\" Servers. Auto changes upon playing a video. - Default Playback Speed : %1$s - Cursed Speeds - Default Resize Mode - Subtitles - Subtitles - Subtitle Color - Subtitle Outline Color - Subtitle Outline Type - Subtitle Background Color - Subtitle Window Color - "The subtitle window is the part left and right from them. (where the background isn\'t)" - Note: Changing above settings only affects Soft-Subtitles! - Subtitle Font - Subtitle Size - - Auto - Autoplay Next Episode - Automatically disables if there is no interaction with player after 1 hour. - Auto Skip Fillers - Skips filler episodes when going to next episode. - - Update Progress - Ask for each Anime \"Individually\" - Ask for each Manga \"Individually\" - Turning off will always automatically update progress when the episode is watched. - Turning off will always automatically update progress when the chapter is read. - Update Progress for Hentai - Update Progress for Doujins - very bold of you sar - Watch Update Percentage - The percentage at which your Anilist progress should be updated after watching an episode. \nThis also sets the \% for when to preload links for the next episode. - - Behaviour - Always Continue from where you left off - Pause when not in Focus - Volume & Brightness Gestures - Double tap to Seek - Fast Forward - Turning off will show fast forward & rewind buttons - Seek Time - Amount of time in seconds for fast forward & rewind. - Skip Time - Setting to 0, hides the Skip Button. - Show Cast Button - Requires \"Web Video Caster\" app to cast. - Picture in Picture - Always Minimize - Requires PiP to be enabled, makes the player behave like Youtube Player but better.\nAlso hides the PiP button. - - App - Hide Status Bar - Requires App restart to fully apply. - Show/Hide Layouts on Home - - Continue Watching - Favourite Anime - Planned Anime - Continue Reading - Favourite Manga - Planned Manga - Recommended - - - Default Start Up Tab - Small View in Trending Shows - Animations - Banner Animations - Layout Animations - Overall Speed - Looks like you don\'t like anything,\nTry liking a show to keep it here. - Favourite Anime - Favourite Manga - Restart the app? - Next - Previous - Current Page - Dubbed - Subbed - Prefer Dubbed Anime - None - Selected DNS - Change if your ISP blocks any source - Keep Screen On - Layout - Spaced Pages - Direction - - General - Show Status & Navigation Bars - Auto Detect Webtoon - If the Manga is not from Japan, the reader will default to Webtoon Reader Settings - Default Settings - Horizontal Scroll Bar - Dual Page - Shows 2 Images in 1 page, will look weird if the images aren\'t the same size - True Colors - (32-bit color Mode) Reduces Banding on the images, may affect performance. - Image Rotation - Hide Page Numbers - Sort by Title - Sort by Last Updated - Sort by Score - Over Scroll to Next/Previous Chapter - Change pages with Volume Buttons - Private - Wrap Images - Mostly useful for larger devices, removes space between images, if they exist. - Reload - Share - Skip - Show Skip Time Stamp Button - Always Load Time Stamps - Time Stamps - Other - Auto Skip OP / ED - Requires Time Stamps to be Enabled - TOTAL REPEATS - Custom Lists - Want to support Dantotsu\'s Maintainer?\nConsider Donating - No donation goal atm - Filter - Year - Apply - Cancel - This Season - Next Season - Previous Season - Include List - Calendar - Planned Anime - Planned Manga - Open image by Long Clicking - Always continue Shows - Useful if you are getting Handshake Fails - Use Proxy for Timestamps - Always check for App Updates - Author - Versions - FAQ - Accounts - MyAnimeList - Login with Anilist! - Anilist - How does this work\? - - Dantotsu is an Anilist Based App, so for syncing with your MAL Account, It needs an Anilist account to be logged in. - \nOnce logged in with both Anilist and MAL accounts, the app will automatically update your MAL account whenever: - \n- Add a new Media - \n- Edit a Media - \n- Delete a Media - - \n\nNote: The app will not sync old Media\'s from Anilist to MAL, It\'s recommended to sync them. - \n- Check __FAQs__ for _Easy Method_ - - \n\nAnd for _Intermediates_ : - \n- [How to sync Anilist data with MAL](https://anilist.co/forum/thread/2654) - \n- [How to sync MAL data with Anilist](https://anilist.co/forum/thread/3393) - - \n\n_It is not required to sync both MAL and Anilist accounts._ - - Show notification for Checking Subscriptions - Notification for Checking Subscriptions - Subscriptions Update Frequency : %1$s - Subscriptions Update Frequency - Amount of time for Dantotsu to periodically check for new Episodes/Chapters\n(Less time will cause more battery consumption) - Don\'t Update - Loading Next Chapter - Grid - Sort by Release Date - Crop Borders - NOTE - - - DAMN! YOU TRULY ARE JOBLESS\nYOU REACHED THE END - Couldn\'t find any File Manager to open SD card - Error loading data %1$s - You Long Click the button to check for App Update - Saved to:\n%s - Setting progress to %1$d - Please Login into anilist account! - Congrats Vro - Please Reload. - Copied "%1$s" - Please perform BACK again to Exit - No Internet Connection - Seems like that wasn\'t found on Anilist. - Disabled Auto Skipping OP & ED - Auto Skipping OP & ED - Copied to Clipboard - This is the 1st Episode! - You can long click List Editor button to Reset Auto Update - Autoplay cancelled, no Interaction for more than 1 Hour. - Couldn\'t auto select the server, Please try again! - Logging in MAL - Getting User Data - No next Episode Found! - Try Enabling Banner Animations from Settings - Please Login with Anilist! - Auto Update Progress has now been Reset-ed - Can\'t Wait, huh? fine X( - Downloading… - Next Chapter Not Found - This is the 1st Chapter! - Adult Stuff?( ͡° ͜ʖ ͡° ) - What did you even open? - Error getting Data from Anilist. - Empty Response, Does your internet perhaps suck? - Error loading MAL User Data - Failed to load data from MAL - Error loading Anilist User Data - Couldn\'t find episode : %1$s - List Updated - Deleted from List - No List ID found, reloading… - Checking for Update - Don\'t show again for version %1$s - No Update Found - Downloading Update %1$s - Please give permission to access Files & Folders from Settings, & Try again. - Started Downloading\n%1$s - Please install 1DM - Please install ADM - Error getting Image Data - Loading Image Failed - Copied device info - Seems like Anilist is down, maybe try using a VPN or you can wait for it to come back. - Failed to load saved data of %1$d - Wasn\'t able to get access - Mal Login : Uri not Found - Mal Login : codeChallenge not found - Mal Login : Code not present in Redirected URI - Refresh Token : Failed to load Saved Token - Refreshing Token Failed - Episode %1$d will be released in - %1$d days %2$d hrs %3$d mins %4$d secs - - Score - Popular - Trending - A-Z - Z-A - What? - - MAIN - SUPPORTING - - FINISHED - RELEASING - NOT YET RELEASED - CANCELLED - HIATUS - - ADAPTATION - PARENT - CHARACTER - SUMMARY - ALTERNATIVE - OTHER - SOURCE - CONTAINS - - Read on Dantotsu - Watch on Dantotsu - "Continue : Episode " - "Continue : " - "Episode " - "Episode %1$s" - "Chapter " - "Chapter %1$s" - - just got released! - Checking Subscriptions - - Speed - Auto Update progress for %1$s? - Continue from %1$s? - Update progress on anilist? - Incognito mode will still ignore progress. - "Don\'t ask again for %1$s" - Default Speed - Default Resize Mode - Primary Sub Color - Outline Sub Color - Outline Type - Subtitle Font - - - Yes - No - Close - No Chapter - Turn on 18+ Content from your Anilist Settings - Available - Let\'s Go - Cope - - "Watched " - "Read " - " out of " - " out of " - "Total of " - "Total of " - "No Description Available" - - - Top to Bottom - Right to Left - Bottom to Top - Left to Right - - - - Paged - Continuous Paged - Continuous - - - Selected - Found - - "__Age:__ " - \n"__Birthday:__ " - \n"__Gender:__ "" - - Male - Female - - What is Dantotsu?\n Why should you use Dantotsu? - Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-of-the-art elegance. It is an Anilist only client, which also lets you stream & download Anime / Manga through extensions. \n>Dantotsu (断トツ; Dan-totsu) literally means the best of the best in Japanese. Well, we would like to say this is the best open source app for anime and manga on Android, but hey, Try it out yourself & judge! - - What are some features of Dantotsu? - Some mentionable features of Dantotsu are\n\n- Easy and functional way to both, watch anime and read manga and light novels, Ad Free.\n- A completely open source app with a nice UI & Animations\n- 3rd party plugin support \n- Synchronize anime and manga real-time with AniList. Easily categorize anime and manga based on your current status. (Powered by AniList)\n- Find all shows using thoroughly and frequently updated list of all trending, popular and ongoing anime based on scores.\n- View extensive details about anime shows, movies and manga titles. It also features ability to countdown to the next episode of airing anime. (Powered by AniList & MyAnimeList) - - What are Artifacts? - Whenever a developer commits or pull requests a feature or fix, GitHub automatically makes an APK file for you to use. This APK is called an Artifact. Artifacts through pull requests may or may not be added to the main release of the app. Artifacts have a higher chance of having bugs and glitches. To know if new artifacts are available, star the Dantotsu repository and turn on notifications\n\nTo download an Artifact:\n1) Sign In/Up in GitHub\n2) Go to Dantotsu\n3) Go to actions\n4) Press on the workflow run you want to download the artifact of.\n5) Press on artifact\n6) Extract the file using a zip extractor\n7) Install and enjoy. - - Is Dantotsu available for PC? - Currently no (for both Windows and Linux). There isn\'t any estimation when it will be available. But you can download any Android emulator and run Dantotsu on it. For Windows 11 users, they can use the Windows Subsystem for Android (WSA) to run Dantotsu in Windows. - - Is Dantotsu available for iOS? - No, and currently no plans to support iOS - - Why are my stats not updating? - This is because it updates every 48 hours automatically (by Anilist). If you really need to update your stats, you can force update your stats after going to this [link](https://anilist.co/settings/lists). - - How to download Episodes? - 1. Download 1DM or ADM from Google Play Store. - \n2. Enter the app, give storage access and set your preferences (downloading speed, downloading path etc(optional)) - \n3. Now go to Dantotsu > Settings > Common > Download Managers and choose the download manager you just set up. - \n4. Go to your desired episode and press on the download icon of any server. There may be a popup to set your preferences again, just press on "Download" and it will be saved in the directed path. - - \n\nNote: Direct downloads are also possible without a manager but it\'s not recommended. - - How to download Manga Chapters? - It is yet not possible to download chapters in Dantotsu but this feature will be implemented soon. - - How to enable NSFW content? - You can enable nsfw content by enabling 18+ contents from this [link](https://anilist.co/settings/media). - - How to import my MAL/Kitsu list to Anilist? - Here is how you do it,\n\nExport:\n\n1. Go to this [link](https://malscraper.azurewebsites.net).\n2. Give your Kitsu/MAL username and download both anime and manga list. (They will be in XML format)\nNote: You have to write the username of the tracker you selected\n\nImport:\n\n1. After exporting your anime and manga list from Kitsu/MAL, now go [here](https://anilist.co/settings/import) \n2. Select/drop the anime XML file on the box above.\n|→Select/drop the manga XML file on the box below. - - How to import my Anilist/Kitsu list to MAL? - Here is how you do it,\n\nExport:\n\n1. Go to this [link](https://malscraper.azurewebsites.net/). \n2. Give your Anilist username/Kitsu ID in the \'Username/Kitsu User ID\' box. \n3. Select list type and enable \'update_on_import\'. \n4. Download the file; it will be in .xml format. Be sure to download both Anime and Manga lists.\n\nImport:\n1. To import it in your MAL account, go to this [link](https://myanimelist.net/import.php) and choose \'MyAnimeList Import\' as import type. \n2. Press on \'Choose File\'and select the downloaded anime/manga list XML file. \n3. Press on \'Import Data\'. \nCongratulations, you just imported the selected list to your MAL account. - - Why can\'t I find a specific anime/manga title? - Let\'s say you are looking for Castlevania in Dantotsu. But Anilist doesn\'t have it, so Dantotsu doesn\'t either.\nA solution to the above problem is as follows:\n1) Go to any anime that\'s not in your list.\n2) Go to the watch section.\n3) Select any source and press on the \'Wrong Title?\'.\n4) Now search for Castlevania (The anime you were looking for) and select it.\n5) ENJOY!\n\nIf you can\'t find the anime even through these steps, then that\'s bad luck for you, bud. Even that source doesn\'t have it. Try a different source. - - How to fix sources selecting a completely wrong title? - Dantotsu itself doesn\'t host anything but relies on other sources. When showing the episodes, it chooses the 1st result given by the source after searching for the title. Dantotsu has no way of detecting if that\'s legit or not. So, for this, we have the \'Wrong Title?\' just below the source name(above layouts). You can choose the correct result/title by pressing on it and enjoy its episodes. - - How to read coloured mangas? - Are you in search of coloured manga? Sorry to break it to you but an extremely small amount of mangas have coloured version. Those which has a coloured version is also available in Dantotsu. Let\'s say you want to read the coloured version of chainsaw man. Then follow the below steps ↓\n\n1) Go to Chainsaw Man\n2) Press on \'Read\'\n3) Select any working source\n4) Press on \'Wrong Title\'\n5) Select the colored version chainsaw man\n6) Enjoy\n\nNote: Many sources don\'t have the coloured version available even if it\'s available somewhere on the internet. So try a different source. If none of the sources have it, then a coloured version of your desired manga simply doesn\'t exist. If you can find it on any manga site on the internet, you can suggest that site through the Discord server. - - Handshake fails? Why are no timestamps not loading? - You can fix this issue by enabling \'Proxy\' from \n\`Settings > Anime > Player Settings > Timestamps > Proxy\`.\nIf the timestamps are still not loading but the handshake failed popup is fixed, then the episode you are watching just doesn\'t have timestamps yet for it. - - Having trouble with a source? - Some basic fixes would be :\n\n• Restart the app. \n• Use a different DNS from your settings, preferably, CloudFlare. \n• VPN might work as well. \n\nIf you refuse to try the above steps then just use a different source.\n\nNote: Allanime fixes itself most of the time. - - Some useful tips and tricks - The following presents some tips and tricks you may or may not know about - \n \n \n - By hold pressing the Dantotsu logo in settings, you can check if there are any new updates manually. \n \n - Hold pressing an error message/tag/synonym or title will copy it. \n \n - You can open an episode with other apps by hold pressing any server for that episode. This helps in streaming the episode using other video players or download the episode using download managers. \n \n - You can set up custom lists using this [link](https://anilist.co/settings/lists). (you need to be signed in) \n \n - If your episode/chapter is not being progressed automatically after you finish watching/reading it, then hold press the status bar(planning/repeating/watching button) of that anime/manga. The next time you start a chapter/finish an episode, you will stumble upon a popup. Press yes there. - - - Subscribed! Receiving notifications, when new episodes are released on %1$s. - - Unsubscribed, you will not receive any notifications. - Episodes - Episode - Chapter - Chapters - - "Format : %1$s" - "Sort : %1$s" - "Not %1$s" - Search by Image - Upload Image - Similarity: %1$s %% - From %1$s to %2$s - Invalid URL - No Anilist ID found - Error loading image - Successfully Logged Out - Try logging-in again - Error loading Discord User Data - - Warning - View Anime - View Manga - Force Legacy Installer - Extensions - NSFW Extensions - Skip loading extension icons - Material You - Extension-specific DNS - Theme: - User Agent - Custom Theme - Custom theme - Color same as Anime/Manga cover - OLED theme variant - Installed Anime - Available Anime - Installed Manga - Color Picker - Random Selection - Incognito Mode - - From af992bd19c525c083b7aef74999006081d1a2719 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Thu, 4 Jan 2024 23:32:06 -0600 Subject: [PATCH 29/29] Delete app/src/main/res/values-en-rDW directory --- app/src/main/res/values-en-rDW/strings.xml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/src/main/res/values-en-rDW/strings.xml diff --git a/app/src/main/res/values-en-rDW/strings.xml b/app/src/main/res/values-en-rDW/strings.xml deleted file mode 100644 index e69de29b..00000000