From c462c19a8ba89a4727663029aa3f548a64b5c00d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 28 Apr 2022 16:46:55 +0300 Subject: [PATCH 01/15] Option to hide 'All categories' tab from favourites --- .../base/ui/widgets/CheckableImageView.kt | 7 ++ .../kotatsu/core/prefs/AppSettings.kt | 13 ++- .../kotatsu/favourites/FavouritesModule.kt | 2 +- .../favourites/domain/FavouritesRepository.kt | 2 +- .../ui/FavouritesContainerFragment.kt | 80 +++++++++++-------- .../favourites/ui/FavouritesPagerAdapter.kt | 33 +++++--- .../ui/FavouritesTabLongClickListener.kt | 4 +- .../categories/AllCategoriesToggleListener.kt | 6 ++ .../ui/categories/CategoriesActivity.kt | 25 ++++-- .../ui/categories/CategoriesAdapter.kt | 32 ++++---- .../FavouritesCategoriesViewModel.kt | 50 +++++++++++- .../ui/categories/adapter/AllCategoriesAD.kt | 20 +++++ .../ui/categories/{ => adapter}/CategoryAD.kt | 10 +-- .../categories/adapter/CategoryListModel.kt | 59 ++++++++++++++ .../sources/SourcesSettingsFragment.kt | 5 +- app/src/main/res/drawable/ic_hidden.xml | 12 +++ app/src/main/res/drawable/ic_shown.xml | 12 +++ app/src/main/res/drawable/ic_shown_hidden.xml | 5 ++ .../main/res/layout/item_categories_all.xml | 33 ++++++++ ...egory_empty.xml => popup_category_all.xml} | 4 + app/src/main/res/values/strings.xml | 1 + 21 files changed, 327 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/AllCategoriesToggleListener.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/AllCategoriesAD.kt rename app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/{ => adapter}/CategoryAD.kt (68%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt create mode 100644 app/src/main/res/drawable/ic_hidden.xml create mode 100644 app/src/main/res/drawable/ic_shown.xml create mode 100644 app/src/main/res/drawable/ic_shown_hidden.xml create mode 100644 app/src/main/res/layout/item_categories_all.xml rename app/src/main/res/menu/{popup_category_empty.xml => popup_category_all.xml} (70%) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableImageView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableImageView.kt index 9c8366293..5d601d67c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableImageView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableImageView.kt @@ -5,6 +5,7 @@ import android.os.Parcel import android.os.Parcelable import android.os.Parcelable.Creator import android.util.AttributeSet +import android.view.View import android.widget.Checkable import androidx.annotation.AttrRes import androidx.appcompat.widget.AppCompatImageView @@ -61,6 +62,12 @@ class CheckableImageView @JvmOverloads constructor( } } + class ToggleOnClickListener : OnClickListener { + override fun onClick(view: View) { + (view as? Checkable)?.toggle() + } + } + fun interface OnCheckedChangeListener { fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 5784153d3..2b65c60a3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -11,10 +11,6 @@ import androidx.collection.arraySetOf import androidx.core.content.edit import androidx.preference.PreferenceManager import com.google.android.material.color.DynamicColors -import java.io.File -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.callbackFlow @@ -24,6 +20,10 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.utils.ext.getEnumValue import org.koitharu.kotatsu.utils.ext.putEnumValue import org.koitharu.kotatsu.utils.ext.toUriOrNull +import java.io.File +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* class AppSettings(context: Context) { @@ -67,6 +67,10 @@ class AppSettings(context: Context) { get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true) set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) } + var isAllFavouritesVisible: Boolean + get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true) + set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) } + val isUpdateCheckingEnabled: Boolean get() = prefs.getBoolean(KEY_APP_UPDATE_AUTO, true) @@ -278,6 +282,7 @@ class AppSettings(context: Context) { const val KEY_SEARCH_SINGLE_SOURCE = "search_single_source" const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism" const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown" + const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt index 9cd6e751e..8222c0f2b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt @@ -15,7 +15,7 @@ val favouritesModule viewModel { categoryId -> FavouritesListViewModel(categoryId.get(), get(), get(), get()) } - viewModel { FavouritesCategoriesViewModel(get()) } + viewModel { FavouritesCategoriesViewModel(get(), get()) } viewModel { manga -> MangaCategoriesViewModel(manga.get(), get()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 5cb19fda5..731d7ff5e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -64,7 +64,7 @@ class FavouritesRepository(private val db: MangaDatabase) { createdAt = System.currentTimeMillis(), sortKey = db.favouriteCategoriesDao.getNextSortKey(), categoryId = 0, - order = SortOrder.UPDATED.name, + order = SortOrder.NEWEST.name, ) val id = db.favouriteCategoriesDao.insert(entity) return entity.toFavouriteCategory(id) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 72b0566b8..4433f978d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -21,12 +21,11 @@ import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel +import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.main.ui.AppBarOwner -import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.ext.resolveDp -import java.util.* class FavouritesContainerFragment : BaseFragment(), @@ -53,15 +52,15 @@ class FavouritesContainerFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val adapter = FavouritesPagerAdapter(this, this) - viewModel.categories.value?.let { - adapter.replaceData(wrapCategories(it)) + viewModel.visibleCategories.value?.let { + adapter.replaceData(it) } binding.pager.adapter = adapter pagerAdapter = adapter TabLayoutMediator(binding.tabs, binding.pager, adapter).attach() actionModeDelegate.addListener(this, viewLifecycleOwner) - viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged) + viewModel.visibleCategories.observe(viewLifecycleOwner, ::onCategoriesChanged) viewModel.onError.observe(viewLifecycleOwner, ::onError) } @@ -86,7 +85,8 @@ class FavouritesContainerFragment : top = headerHeight - insets.top ) binding.pager.updatePadding( - top = -headerHeight + resources.resolveDp(8) // 8 dp is needed so that the top of the list is not attached to tabs (visible when ActionMode is active) + // 8 dp is needed so that the top of the list is not attached to tabs (visible when ActionMode is active) + top = -headerHeight + resources.resolveDp(8) ) binding.tabs.apply { updatePadding( @@ -99,8 +99,8 @@ class FavouritesContainerFragment : } } - private fun onCategoriesChanged(categories: List) { - pagerAdapter?.replaceData(wrapCategories(categories)) + private fun onCategoriesChanged(categories: List) { + pagerAdapter?.replaceData(categories) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -122,26 +122,11 @@ class FavouritesContainerFragment : Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() } - override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean { - val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category - val menu = PopupMenu(tabView.context, tabView) - menu.inflate(menuRes) - createOrderSubmenu(menu.menu, category) - menu.setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_remove -> editDelegate.deleteCategory(category) - R.id.action_rename -> editDelegate.renameCategory(category) - R.id.action_create -> editDelegate.createCategory() - R.id.action_order -> return@setOnMenuItemClickListener false - else -> { - val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order) - ?: return@setOnMenuItemClickListener false - viewModel.setCategoryOrder(category.id, order) - } - } - true + override fun onTabLongClick(tabView: View, item: CategoryListModel): Boolean { + when (item) { + is CategoryListModel.All -> showAllCategoriesMenu(tabView) + is CategoryListModel.CategoryItem -> showCategoryMenu(tabView, item.category) } - menu.show() return true } @@ -157,13 +142,6 @@ class FavouritesContainerFragment : viewModel.createCategory(name) } - private fun wrapCategories(categories: List): List { - val data = ArrayList(categories.size + 1) - data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, SortOrder.NEWEST, Date()) - data += categories - return data - } - private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) { val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { @@ -181,6 +159,40 @@ class FavouritesContainerFragment : } } + private fun showCategoryMenu(tabView: View, category: FavouriteCategory) { + val menu = PopupMenu(tabView.context, tabView) + menu.inflate(R.menu.popup_category) + createOrderSubmenu(menu.menu, category) + menu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_remove -> editDelegate.deleteCategory(category) + R.id.action_rename -> editDelegate.renameCategory(category) + R.id.action_create -> editDelegate.createCategory() + R.id.action_order -> return@setOnMenuItemClickListener false + else -> { + val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order) + ?: return@setOnMenuItemClickListener false + viewModel.setCategoryOrder(category.id, order) + } + } + true + } + menu.show() + } + + private fun showAllCategoriesMenu(tabView: View) { + val menu = PopupMenu(tabView.context, tabView) + menu.inflate(R.menu.popup_category_all) + menu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_create -> editDelegate.createCategory() + R.id.action_hide -> viewModel.setAllCategoriesVisible(false) + } + true + } + menu.show() + } + companion object { fun newInstance() = FavouritesContainerFragment() diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesPagerAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesPagerAdapter.kt index 29de80809..329e06751 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesPagerAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesPagerAdapter.kt @@ -7,14 +7,16 @@ import androidx.recyclerview.widget.DiffUtil import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator -import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment class FavouritesPagerAdapter( fragment: Fragment, private val longClickListener: FavouritesTabLongClickListener ) : FragmentStateAdapter(fragment.childFragmentManager, fragment.viewLifecycleOwner.lifecycle), - TabLayoutMediator.TabConfigurationStrategy, View.OnLongClickListener { + TabLayoutMediator.TabConfigurationStrategy, + View.OnLongClickListener { private val differ = AsyncListDiffer(this, DiffCallback()) @@ -35,12 +37,15 @@ class FavouritesPagerAdapter( override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { val item = differ.currentList[position] - tab.text = item.title + tab.text = when (item) { + is CategoryListModel.All -> tab.view.context.getString(R.string.all_favourites) + is CategoryListModel.CategoryItem -> item.category.title + } tab.view.tag = item.id tab.view.setOnLongClickListener(this) } - fun replaceData(data: List) { + fun replaceData(data: List) { differ.submitList(data) } @@ -50,16 +55,22 @@ class FavouritesPagerAdapter( return longClickListener.onTabLongClick(v, item) } - private class DiffCallback : DiffUtil.ItemCallback() { + private class DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: FavouriteCategory, - newItem: FavouriteCategory - ): Boolean = oldItem.id == newItem.id + oldItem: CategoryListModel, + newItem: CategoryListModel + ): Boolean = when { + oldItem is CategoryListModel.All && newItem is CategoryListModel.All -> true + oldItem is CategoryListModel.CategoryItem && newItem is CategoryListModel.CategoryItem -> { + oldItem.category.id == newItem.category.id + } + else -> false + } override fun areContentsTheSame( - oldItem: FavouriteCategory, - newItem: FavouriteCategory - ): Boolean = oldItem.id == newItem.id && oldItem.title == newItem.title + oldItem: CategoryListModel, + newItem: CategoryListModel + ): Boolean = oldItem == newItem } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesTabLongClickListener.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesTabLongClickListener.kt index acc4f89c0..13fca87c9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesTabLongClickListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesTabLongClickListener.kt @@ -1,9 +1,9 @@ package org.koitharu.kotatsu.favourites.ui import android.view.View -import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel fun interface FavouritesTabLongClickListener { - fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean + fun onTabLongClick(tabView: View, item: CategoryListModel): Boolean } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/AllCategoriesToggleListener.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/AllCategoriesToggleListener.kt new file mode 100644 index 000000000..380722b84 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/AllCategoriesToggleListener.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.favourites.ui.categories + +interface AllCategoriesToggleListener { + + fun onAllCategoriesToggle(isVisible: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt index 9caeb87c8..5a2eaf8df 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt @@ -21,6 +21,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding +import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight @@ -29,7 +30,7 @@ class CategoriesActivity : BaseActivity(), OnListItemClickListener, View.OnClickListener, - CategoriesEditDelegate.CategoriesEditCallback { + CategoriesEditDelegate.CategoriesEditCallback, AllCategoriesToggleListener { private val viewModel by viewModel() @@ -41,7 +42,7 @@ class CategoriesActivity : super.onCreate(savedInstanceState) setContentView(ActivityCategoriesBinding.inflate(layoutInflater)) supportActionBar?.setDisplayHomeAsUpEnabled(true) - adapter = CategoriesAdapter(this) + adapter = CategoriesAdapter(this, this) editDelegate = CategoriesEditDelegate(this, this) binding.recyclerView.setHasFixedSize(true) binding.recyclerView.adapter = adapter @@ -49,7 +50,7 @@ class CategoriesActivity : reorderHelper = ItemTouchHelper(ReorderHelperCallback()) reorderHelper.attachToRecyclerView(binding.recyclerView) - viewModel.categories.observe(this, ::onCategoriesChanged) + viewModel.allCategories.observe(this, ::onCategoriesChanged) viewModel.onError.observe(this, ::onError) } @@ -84,6 +85,10 @@ class CategoriesActivity : return true } + override fun onAllCategoriesToggle(isVisible: Boolean) { + viewModel.setAllCategoriesVisible(isVisible) + } + override fun onWindowInsetsChanged(insets: Insets) { binding.fabAdd.updateLayoutParams { rightMargin = topMargin + insets.right @@ -97,7 +102,7 @@ class CategoriesActivity : ) } - private fun onCategoriesChanged(categories: List) { + private fun onCategoriesChanged(categories: List) { adapter.items = categories binding.textViewHolder.isVisible = categories.isEmpty() } @@ -138,13 +143,19 @@ class CategoriesActivity : ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0 ) { + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit + override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder, - ): Boolean = true + ): Boolean = viewHolder.itemViewType == target.itemViewType - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit + override fun canDropOver( + recyclerView: RecyclerView, + current: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean = current.itemViewType == target.itemViewType override fun onMoved( recyclerView: RecyclerView, @@ -158,6 +169,8 @@ class CategoriesActivity : super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y) viewModel.reorderCategories(fromPos, toPos) } + + override fun isLongPressDragEnabled(): Boolean = false } companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt index adf19ca9c..e13b31e00 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt @@ -4,13 +4,18 @@ import androidx.recyclerview.widget.DiffUtil import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel +import org.koitharu.kotatsu.favourites.ui.categories.adapter.allCategoriesAD +import org.koitharu.kotatsu.favourites.ui.categories.adapter.categoryAD class CategoriesAdapter( onItemClickListener: OnListItemClickListener, -) : AsyncListDifferDelegationAdapter(DiffCallback()) { + allCategoriesToggleListener: AllCategoriesToggleListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { delegatesManager.addDelegate(categoryAD(onItemClickListener)) + .addDelegate(allCategoriesAD(allCategoriesToggleListener)) setHasStableIds(true) } @@ -18,28 +23,23 @@ class CategoriesAdapter( return items[position].id } - private class DiffCallback : DiffUtil.ItemCallback() { + private class DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: FavouriteCategory, - newItem: FavouriteCategory, - ): Boolean { - return oldItem.id == newItem.id - } + oldItem: CategoryListModel, + newItem: CategoryListModel, + ): Boolean = oldItem.id == newItem.id override fun areContentsTheSame( - oldItem: FavouriteCategory, - newItem: FavouriteCategory, - ): Boolean { - return oldItem.id == newItem.id && oldItem.title == newItem.title - && oldItem.order == newItem.order - } + oldItem: CategoryListModel, + newItem: CategoryListModel, + ): Boolean = oldItem == newItem override fun getChangePayload( - oldItem: FavouriteCategory, - newItem: FavouriteCategory, + oldItem: CategoryListModel, + newItem: CategoryListModel, ): Any? = when { - oldItem.title == newItem.title && oldItem.order != newItem.order -> newItem.order + oldItem is CategoryListModel.All && newItem is CategoryListModel.All -> Unit else -> super.getChangePayload(oldItem, newItem) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index c87e44c76..7aac74e62 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -3,20 +3,36 @@ package org.koitharu.kotatsu.favourites.ui.categories import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository +import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import java.util.* class FavouritesCategoriesViewModel( - private val repository: FavouritesRepository + private val repository: FavouritesRepository, + private val settings: AppSettings, ) : BaseViewModel() { private var reorderJob: Job? = null - val categories = repository.observeCategories() - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + val allCategories = combine( + repository.observeCategories(), + observeAllCategoriesVisible(), + ) { list, showAll -> + mapCategories(list, showAll, true) + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + + val visibleCategories = combine( + repository.observeCategories(), + observeAllCategoriesVisible(), + ) { list, showAll -> + mapCategories(list, showAll, showAll) + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) fun createCategory(name: String) { launchJob { @@ -42,14 +58,40 @@ class FavouritesCategoriesViewModel( } } + fun setAllCategoriesVisible(isVisible: Boolean) { + settings.isAllFavouritesVisible = isVisible + } + fun reorderCategories(oldPos: Int, newPos: Int) { val prevJob = reorderJob reorderJob = launchJob(Dispatchers.Default) { prevJob?.join() - val items = categories.value ?: error("This should not happen") + val items = allCategories.value ?: error("This should not happen") val ids = items.mapTo(ArrayList(items.size)) { it.id } Collections.swap(ids, oldPos, newPos) + ids.remove(0L) repository.reorderCategories(ids) } } + + private fun mapCategories( + categories: List, + isAllCategoriesVisible: Boolean, + withAllCategoriesItem: Boolean, + ): List { + val result = ArrayList(categories.size + 1) + if (withAllCategoriesItem) { + result.add(CategoryListModel.All(isAllCategoriesVisible)) + } + categories.mapTo(result) { + CategoryListModel.CategoryItem(it) + } + return result + } + + private fun observeAllCategoriesVisible() = settings.observe() + .filter { it == AppSettings.KEY_ALL_FAVOURITES_VISIBLE } + .map { settings.isAllFavouritesVisible } + .onStart { emit(settings.isAllFavouritesVisible) } + .distinctUntilChanged() } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/AllCategoriesAD.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/AllCategoriesAD.kt new file mode 100644 index 000000000..113198bfa --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/AllCategoriesAD.kt @@ -0,0 +1,20 @@ +package org.koitharu.kotatsu.favourites.ui.categories.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.databinding.ItemCategoriesAllBinding +import org.koitharu.kotatsu.favourites.ui.categories.AllCategoriesToggleListener + +fun allCategoriesAD( + allCategoriesToggleListener: AllCategoriesToggleListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemCategoriesAllBinding.inflate(inflater, parent, false) } +) { + + binding.imageViewToggle.setOnClickListener { + allCategoriesToggleListener.onAllCategoriesToggle(!item.isVisible) + } + + bind { + binding.imageViewToggle.isChecked = item.isVisible + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt similarity index 68% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoryAD.kt rename to app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt index 97b38d926..d840b783f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoryAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.favourites.ui.categories +package org.koitharu.kotatsu.favourites.ui.categories.adapter import android.view.MotionEvent import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding @@ -8,23 +8,23 @@ import org.koitharu.kotatsu.databinding.ItemCategoryBinding fun categoryAD( clickListener: OnListItemClickListener -) = adapterDelegateViewBinding( +) = adapterDelegateViewBinding( { inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) } ) { binding.imageViewMore.setOnClickListener { - clickListener.onItemClick(item, it) + clickListener.onItemClick(item.category, it) } @Suppress("ClickableViewAccessibility") binding.imageViewHandle.setOnTouchListener { v, event -> if (event.actionMasked == MotionEvent.ACTION_DOWN) { - clickListener.onItemLongClick(item, itemView) + clickListener.onItemLongClick(item.category, itemView) } else { false } } bind { - binding.textViewTitle.text = item.title + binding.textViewTitle.text = item.category.title } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt new file mode 100644 index 000000000..8326f8617 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt @@ -0,0 +1,59 @@ +package org.koitharu.kotatsu.favourites.ui.categories.adapter + +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.list.ui.model.ListModel + +sealed interface CategoryListModel : ListModel { + + val id: Long + + class All( + val isVisible: Boolean, + ) : CategoryListModel { + + override val id: Long = 0L + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as All + + if (isVisible != other.isVisible) return false + + return true + } + + override fun hashCode(): Int { + return isVisible.hashCode() + } + } + + class CategoryItem( + val category: FavouriteCategory, + ) : CategoryListModel { + + override val id: Long + get() = category.id + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CategoryItem + + if (category.id != other.category.id) return false + if (category.title != other.category.title) return false + if (category.order != other.category.order) return false + + return true + } + + override fun hashCode(): Int { + var result = category.id.hashCode() + result = 31 * result + category.title.hashCode() + result = 31 * result + category.order.hashCode() + return result + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index 60a5b6ff9..ad27cec65 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -141,10 +141,7 @@ class SourcesSettingsFragment : BaseFragment(), recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder, - ): Boolean = current.itemViewType == target.itemViewType && viewModel.canReorder( - current.bindingAdapterPosition, - target.bindingAdapterPosition, - ) + ): Boolean = current.itemViewType == target.itemViewType override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit diff --git a/app/src/main/res/drawable/ic_hidden.xml b/app/src/main/res/drawable/ic_hidden.xml new file mode 100644 index 000000000..82816e502 --- /dev/null +++ b/app/src/main/res/drawable/ic_hidden.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shown.xml b/app/src/main/res/drawable/ic_shown.xml new file mode 100644 index 000000000..ee4887a82 --- /dev/null +++ b/app/src/main/res/drawable/ic_shown.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shown_hidden.xml b/app/src/main/res/drawable/ic_shown_hidden.xml new file mode 100644 index 000000000..5405a4747 --- /dev/null +++ b/app/src/main/res/drawable/ic_shown_hidden.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_categories_all.xml b/app/src/main/res/layout/item_categories_all.xml new file mode 100644 index 000000000..8f965adf0 --- /dev/null +++ b/app/src/main/res/layout/item_categories_all.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/popup_category_empty.xml b/app/src/main/res/menu/popup_category_all.xml similarity index 70% rename from app/src/main/res/menu/popup_category_empty.xml rename to app/src/main/res/menu/popup_category_all.xml index 740312ef1..60b3bc3bd 100644 --- a/app/src/main/res/menu/popup_category_empty.xml +++ b/app/src/main/res/menu/popup_category_all.xml @@ -2,6 +2,10 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 828ad9fe6..cca5b5410 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -279,4 +279,5 @@ Helps avoid blocking your IP address Saved manga processing Chapters will be removed in the background. It can take some time + Hide \ No newline at end of file From 714b708fa9c5429fe4c370ddecbe4695fe396887 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 29 Apr 2022 09:03:39 +0300 Subject: [PATCH 02/15] Fix npe on getExternalFilesDirs #158 --- .../koitharu/kotatsu/local/data/LocalStorageManager.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt index 2cbddf856..6e9c1e399 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt @@ -4,6 +4,7 @@ import android.content.ContentResolver import android.content.Context import android.os.StatFs import androidx.annotation.WorkerThread +import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext @@ -11,7 +12,6 @@ import okhttp3.Cache import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.utils.ext.computeSize import org.koitharu.kotatsu.utils.ext.getStorageName -import java.io.File private const val DIR_NAME = "manga" private const val CACHE_DISK_PERCENTAGE = 0.02 @@ -71,7 +71,7 @@ class LocalStorageManager( private fun getAvailableStorageDirs(): MutableSet { val result = LinkedHashSet() result += File(context.filesDir, DIR_NAME) - result += context.getExternalFilesDirs(DIR_NAME) + context.getExternalFilesDirs(DIR_NAME).filterNotNullTo(result) result.retainAll { it.exists() || it.mkdirs() } return result } @@ -87,8 +87,8 @@ class LocalStorageManager( private fun getCacheDirs(subDir: String): MutableSet { val result = LinkedHashSet() result += File(context.cacheDir, subDir) - context.externalCacheDirs.mapTo(result) { - File(it, subDir) + context.externalCacheDirs.mapNotNullTo(result) { + File(it ?: return@mapNotNullTo null, subDir) } return result } @@ -110,4 +110,4 @@ class LocalStorageManager( private fun File.isWriteable() = runCatching { canWrite() }.getOrDefault(false) -} +} \ No newline at end of file From 684b494edbd5d56c0a8a1aa02f93cd79dd99814e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 29 Apr 2022 10:07:04 +0300 Subject: [PATCH 03/15] Fix concurrent manga downloading #154 --- .../download/domain/DownloadManager.kt | 27 ++++---- .../local/domain/LocalMangaRepository.kt | 27 ++++++-- .../koitharu/kotatsu/utils/CompositeMutex.kt | 66 +++++++++++++++++++ .../utils/progress/TimeLeftEstimator.kt | 5 +- 4 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt index b8183a96b..58335ed31 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.download.domain import android.content.Context -import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.webkit.MimeTypeMap import coil.ImageLoader @@ -75,10 +74,12 @@ class DownloadManager( ): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) { @Suppress("NAME_SHADOWING") var manga = manga val chaptersIdsSet = chaptersIds?.toMutableSet() + val cover = loadCover(manga) + outState.value = DownloadState.Queued(startId, manga, cover) + localMangaRepository.lockManga(manga.id) semaphore.acquire() coroutineContext[WakeLockNode]?.acquire() outState.value = DownloadState.Preparing(startId, manga, null) - var cover: Drawable? = null val destination = localMangaRepository.getOutputDir() checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) } val tempFileName = "${manga.id}_$startId.tmp" @@ -88,16 +89,6 @@ class DownloadManager( manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance") } val repo = MangaRepository(manga.source) - cover = runCatching { - imageLoader.execute( - ImageRequest.Builder(context) - .data(manga.coverUrl) - .referer(manga.publicUrl) - .size(coverWidth, coverHeight) - .scale(Scale.FILL) - .build() - ).drawable - }.getOrNull() outState.value = DownloadState.Preparing(startId, manga, cover) val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga output = CbzMangaOutput.get(destination, data) @@ -176,6 +167,7 @@ class DownloadManager( } coroutineContext[WakeLockNode]?.release() semaphore.release() + localMangaRepository.unlockManga(manga.id) } } @@ -208,6 +200,17 @@ class DownloadManager( ) } + private suspend fun loadCover(manga: Manga) = runCatching { + imageLoader.execute( + ImageRequest.Builder(context) + .data(manga.coverUrl) + .referer(manga.publicUrl) + .size(coverWidth, coverHeight) + .scale(Scale.FILL) + .build() + ).drawable + }.getOrNull() + class Factory( private val context: Context, private val imageLoader: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 43860e451..e034d0672 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -18,6 +18,7 @@ import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.parsers.util.toCamelCase import org.koitharu.kotatsu.utils.AlphanumComparator +import org.koitharu.kotatsu.utils.CompositeMutex import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.resolveName @@ -34,6 +35,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma override val source = MangaSource.LOCAL private val filenameFilter = CbzFilter() + private val locks = CompositeMutex() override suspend fun getList( offset: Int, @@ -112,11 +114,18 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma return file.deleteAwait() } - suspend fun deleteChapters(manga: Manga, ids: Set) = runInterruptible(Dispatchers.IO) { - val uri = Uri.parse(manga.url) - val file = uri.toFile() - val cbz = CbzMangaOutput(file, manga) - CbzMangaOutput.filterChapters(cbz, ids) + suspend fun deleteChapters(manga: Manga, ids: Set) { + lockManga(manga.id) + try { + runInterruptible(Dispatchers.IO) { + val uri = Uri.parse(manga.url) + val file = uri.toFile() + val cbz = CbzMangaOutput(file, manga) + CbzMangaOutput.filterChapters(cbz, ids) + } + } finally { + unlockManga(manga.id) + } } @WorkerThread @@ -278,6 +287,14 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma } } + suspend fun lockManga(id: Long) { + locks.lock(id) + } + + suspend fun unlockManga(id: Long) { + locks.unlock(id) + } + private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir -> dir.listFiles(filenameFilter)?.toList().orEmpty() } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt b/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt new file mode 100644 index 000000000..e66355588 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt @@ -0,0 +1,66 @@ +package org.koitharu.kotatsu.utils + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.util.* +import kotlin.coroutines.resume + +class CompositeMutex : Set { + + private val data = HashMap>>() + private val mutex = Mutex() + + override val size: Int + get() = data.size + + override fun contains(element: T): Boolean { + return data.containsKey(element) + } + + override fun containsAll(elements: Collection): Boolean { + return elements.all { x -> data.containsKey(x) } + } + + override fun isEmpty(): Boolean { + return data.isEmpty() + } + + override fun iterator(): Iterator { + return data.keys.iterator() + } + + suspend fun lock(element: T) { + waitForRemoval(element) + mutex.withLock { + val lastValue = data.put(element, LinkedList>()) + check(lastValue == null) { + "CompositeMutex is double-locked for $element" + } + } + } + + suspend fun unlock(element: T) { + val continuations = mutex.withLock { + checkNotNull(data.remove(element)) { + "CompositeMutex is not locked for $element" + } + } + continuations.forEach { c -> + if (c.isActive) { + c.resume(Unit) + } + } + } + + private suspend fun waitForRemoval(element: T) { + val list = data[element] ?: return + suspendCancellableCoroutine { continuation -> + list.add(continuation) + continuation.invokeOnCancellation { + list.remove(continuation) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt index 5cb7aafc5..f998a5119 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.utils.progress import android.os.SystemClock +import java.util.concurrent.TimeUnit import kotlin.math.roundToInt import kotlin.math.roundToLong @@ -11,6 +12,7 @@ class TimeLeftEstimator { private var times = ArrayList() private var lastTick: Tick? = null + private val tooLargeTime = TimeUnit.DAYS.toMillis(1) fun tick(value: Int, total: Int) { if (total < 0) { @@ -36,7 +38,8 @@ class TimeLeftEstimator { } val timePerTick = times.average() val ticksLeft = progress.total - progress.value - return (ticksLeft * timePerTick).roundToLong() + val eta = (ticksLeft * timePerTick).roundToLong() + return if (eta < tooLargeTime) eta else NO_TIME } private class Tick( From 4987d43042c4f4c29b4a146bb882a9886e7a4471 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 29 Apr 2022 11:41:14 +0300 Subject: [PATCH 04/15] Fix pages saving #151 --- .../org/koitharu/kotatsu/local/LocalModule.kt | 3 - .../koitharu/kotatsu/reader/ReaderModule.kt | 6 +- .../kotatsu/reader/domain/PageLoader.kt | 6 +- .../kotatsu/reader/ui/PageSaveHelper.kt | 60 +++++++++++++++++++ .../kotatsu/reader/ui/ReaderActivity.kt | 10 +--- .../kotatsu/reader/ui/ReaderViewModel.kt | 28 ++++++--- .../kotatsu/utils/ExternalStorageHelper.kt | 26 -------- 7 files changed, 93 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ExternalStorageHelper.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt index 928fe706b..3366248a4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt @@ -7,7 +7,6 @@ import org.koitharu.kotatsu.download.domain.DownloadManager import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.ui.LocalListViewModel -import org.koitharu.kotatsu.utils.ExternalStorageHelper val localModule get() = module { @@ -15,8 +14,6 @@ val localModule single { LocalStorageManager(androidContext(), get()) } single { LocalMangaRepository(get()) } - factory { ExternalStorageHelper(androidContext()) } - factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) } viewModel { LocalListViewModel(get(), get(), get(), get()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt index 0d0aec467..f27f061c6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt @@ -1,9 +1,11 @@ package org.koitharu.kotatsu.reader +import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.local.data.PagesCache +import org.koitharu.kotatsu.reader.ui.PageSaveHelper import org.koitharu.kotatsu.reader.ui.ReaderViewModel val readerModule @@ -12,6 +14,8 @@ val readerModule single { MangaDataRepository(get()) } single { PagesCache(get()) } + factory { PageSaveHelper(get(), androidContext()) } + viewModel { params -> ReaderViewModel( intent = params[0], @@ -21,7 +25,7 @@ val readerModule historyRepository = get(), shortcutsRepository = get(), settings = get(), - externalStorageHelper = get(), + pageSaveHelper = get(), ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 523a18881..696f48cc3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -113,6 +113,10 @@ class PageLoader : KoinComponent, Closeable { } } + suspend fun getPageUrl(page: MangaPage): String { + return getRepository(page.source).getPageUrl(page) + } + private fun onIdle() { synchronized(prefetchQueue) { while (prefetchQueue.isNotEmpty()) { @@ -151,7 +155,7 @@ class PageLoader : KoinComponent, Closeable { } private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow): File { - val pageUrl = getRepository(page.source).getPageUrl(page) + val pageUrl = getPageUrl(page) check(pageUrl.isNotBlank()) { "Cannot obtain full image url" } val uri = Uri.parse(pageUrl) return if (uri.scheme == "cbz") { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt new file mode 100644 index 000000000..a2d8f7ac5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt @@ -0,0 +1,60 @@ +package org.koitharu.kotatsu.reader.ui + +import android.content.Context +import android.net.Uri +import androidx.activity.result.ActivityResultLauncher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.HttpUrl.Companion.toHttpUrl +import okio.IOException +import org.koitharu.kotatsu.local.data.PagesCache +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.reader.domain.PageLoader +import kotlin.coroutines.Continuation +import kotlin.coroutines.coroutineContext +import kotlin.coroutines.resume + +class PageSaveHelper( + private val cache: PagesCache, + context: Context, +) { + + private var continuation: Continuation? = null + private val contentResolver = context.contentResolver + + suspend fun savePage( + pageLoader: PageLoader, + page: MangaPage, + saveLauncher: ActivityResultLauncher, + ): Uri { + var pageFile = cache[page.url] + var fileName = pageFile?.name + if (fileName == null) { + fileName = pageLoader.getPageUrl(page).toHttpUrl().pathSegments.last() + } + val cc = coroutineContext + val destination = suspendCancellableCoroutine { cont -> + continuation = cont + Dispatchers.Main.dispatch(cc) { + saveLauncher.launch(fileName) + } + } + continuation = null + if (pageFile == null) { + pageFile = pageLoader.loadPage(page, force = false) + } + runInterruptible(Dispatchers.IO) { + contentResolver.openOutputStream(destination)?.use { output -> + pageFile.inputStream().use { input -> + input.copyTo(output) + } + } ?: throw IOException("Output stream is null") + } + return destination + } + + fun onActivityResult(uri: Uri): Boolean = continuation?.apply { + resume(uri) + } != null +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 914b96ae3..ca9228207 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -9,7 +9,6 @@ import android.view.* import android.widget.Toast import androidx.activity.result.ActivityResultCallback import androidx.core.graphics.Insets -import androidx.core.net.toUri import androidx.core.view.* import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope @@ -187,10 +186,7 @@ class ReaderActivity : R.id.action_save_page -> { viewModel.getCurrentPage()?.also { page -> viewModel.saveCurrentState(reader?.getCurrentState()) - val name = page.url.toUri().run { - fragment ?: lastPathSegment ?: "" - } - savePageRequest.launch(name) + viewModel.saveCurrentPage(page, savePageRequest) } ?: showWaitWhileLoading() } else -> return super.onOptionsItemSelected(item) @@ -199,9 +195,7 @@ class ReaderActivity : } override fun onActivityResult(uri: Uri?) { - if (uri != null) { - viewModel.saveCurrentPage(uri) - } + viewModel.onActivityResult(uri) } private fun onLoadingStateChanged(isLoading: Boolean) { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 688cca84f..5a746c1a3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui import android.net.Uri import android.util.LongSparseArray +import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* @@ -26,7 +27,6 @@ import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState -import org.koitharu.kotatsu.utils.ExternalStorageHelper import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.IgnoreErrors import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct @@ -40,10 +40,11 @@ class ReaderViewModel( private val historyRepository: HistoryRepository, private val shortcutsRepository: ShortcutsRepository, private val settings: AppSettings, - private val externalStorageHelper: ExternalStorageHelper, + private val pageSaveHelper: PageSaveHelper, ) : BaseViewModel() { private var loadingJob: Job? = null + private var pageSaveJob: Job? = null private val currentState = MutableStateFlow(initialState) private val mangaData = MutableStateFlow(intent.manga) private val chapters = LongSparseArray() @@ -137,12 +138,16 @@ class ReaderViewModel( return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() } } - fun saveCurrentPage(destination: Uri) { - launchJob(Dispatchers.Default) { + fun saveCurrentPage( + page: MangaPage, + saveLauncher: ActivityResultLauncher, + ) { + val prevJob = pageSaveJob + pageSaveJob = launchLoadingJob(Dispatchers.Default) { + prevJob?.cancelAndJoin() try { - val page = getCurrentPage() ?: error("Page not found") - externalStorageHelper.savePage(page, destination) - onPageSaved.postCall(destination) + val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher) + onPageSaved.postCall(dest) } catch (e: CancellationException) { throw e } catch (e: Exception) { @@ -154,6 +159,15 @@ class ReaderViewModel( } } + fun onActivityResult(uri: Uri?) { + if (uri != null) { + pageSaveHelper.onActivityResult(uri) + } else { + pageSaveJob?.cancel() + pageSaveJob = null + } + } + fun getCurrentPage(): MangaPage? { val state = currentState.value ?: return null return content.value?.pages?.find { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ExternalStorageHelper.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ExternalStorageHelper.kt deleted file mode 100644 index b769645b8..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ExternalStorageHelper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.koitharu.kotatsu.utils - -import android.content.Context -import android.net.Uri -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible -import okio.IOException -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.reader.domain.PageLoader - -class ExternalStorageHelper(context: Context) { - - private val contentResolver = context.contentResolver - - suspend fun savePage(page: MangaPage, destination: Uri) { - val pageLoader = PageLoader() - val pageFile = pageLoader.loadPage(page, force = false) - runInterruptible(Dispatchers.IO) { - contentResolver.openOutputStream(destination)?.use { output -> - pageFile.inputStream().use { input -> - input.copyTo(output) - } - } ?: throw IOException("Output stream is null") - } - } -} \ No newline at end of file From 3d68d7c8181f568d8ae41e11932055138ab76ef8 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 29 Apr 2022 12:45:28 +0300 Subject: [PATCH 05/15] Reuse PageLoader in PagesThumbnailsSheet --- .../org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt | 2 +- .../reader/ui/thumbnails/PagesThumbnailsSheet.kt | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 5a746c1a3..bfe5ac663 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -55,7 +55,7 @@ class ReaderViewModel( val onPageSaved = SingleLiveEvent() val uiState = combine( mangaData, - currentState + currentState, ) { manga, state -> val chapter = state?.chapterId?.let(chapters::get) ReaderUiState( diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt index f9c6ea3d6..55d59adea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior import org.koin.android.ext.android.get +import org.koin.androidx.viewmodel.ext.android.getViewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener @@ -20,6 +21,8 @@ import org.koitharu.kotatsu.databinding.SheetPagesBinding import org.koitharu.kotatsu.list.ui.MangaListSpanResolver import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.PageLoader +import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.reader.ui.ReaderViewModel import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter import org.koitharu.kotatsu.utils.BottomSheetToolbarController import org.koitharu.kotatsu.utils.ext.viewLifecycleScope @@ -81,7 +84,7 @@ class PagesThumbnailsSheet : dataSet = thumbnails, coil = get(), scope = viewLifecycleScope, - loader = PageLoader().also { pageLoader = it }, + loader = getPageLoader(), clickListener = this@PagesThumbnailsSheet ) addOnLayoutChangeListener(spanResolver) @@ -109,6 +112,11 @@ class PagesThumbnailsSheet : } } + private fun getPageLoader(): PageLoader { + val viewModel = (activity as? ReaderActivity)?.getViewModel() + return viewModel?.pageLoader ?: PageLoader().also { pageLoader = it } + } + private inner class ToolbarController(toolbar: Toolbar) : BottomSheetToolbarController(toolbar) { override fun onStateChanged(bottomSheet: View, newState: Int) { super.onStateChanged(bottomSheet, newState) From 2dce65a44877f1cd0f774d438bd704b80057f800 Mon Sep 17 00:00:00 2001 From: mondstern Date: Sun, 1 May 2022 02:12:03 +0200 Subject: [PATCH 06/15] Translated using Weblate (German) Currently translated at 98.9% (276 of 279 strings) Co-authored-by: mondstern Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ Translation: Kotatsu/Strings --- app/src/main/res/values-de/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a26fd04e6..8933fe241 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -272,4 +272,6 @@ Ausgewählte Elemente dauerhaft vom Gerät löschen\? Sind Sie sicher, dass Sie alle ausgewählten Mangas mit allen Kapiteln herunterladen möchten\? Diese Aktion kann eine Menge Datenverkehr und Speicherplatz verbrauchen Entfernung abgeschlossen + Download-Verzögerung + Parallele Downloads \ No newline at end of file From a2b16990478b90cb098d76b5b4ae3b80d994e8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sun, 1 May 2022 02:12:03 +0200 Subject: [PATCH 07/15] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (280 of 280 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (279 of 279 strings) Co-authored-by: Oğuz Ersen Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/ Translation: Kotatsu/Strings --- app/src/main/res/values-tr/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index fcd97d286..ff48edbc1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -272,4 +272,10 @@ Seçilen ögeler aygıttan kalıcı olarak silinsin mi\? Seçilen tüm mangaları tüm bölümleriyle birlikte indirmek istediğinizden emin misiniz\? Bu işlem çok fazla trafik ve depolama alanı tüketebilir Kaldırma tamamlandı + Bölümler arka planda kaldırılacaktır. Bu biraz zaman alabilir + Paralel indirmeler + İndirmeyi yavaşlat + IP adresinizin engellenmesinden kaçınmanıza yardımcı olur + Kaydedilen manga işleme + Gizle \ No newline at end of file From 9aa28f6fd23bc5367cabb6a3691023449708eb8c Mon Sep 17 00:00:00 2001 From: kuragehime Date: Sun, 1 May 2022 02:12:03 +0200 Subject: [PATCH 08/15] Translated using Weblate (Japanese) Currently translated at 100.0% (280 of 280 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (279 of 279 strings) Co-authored-by: kuragehime Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/ Translation: Kotatsu/Strings --- app/src/main/res/values-ja/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7aa767327..d6db95de8 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -272,4 +272,10 @@ 選択した項目をデバイスから完全に削除しますか? 削除が完了しました 本当に選択したマンガを全編ダウンロードしますか?この動作は多くのトラフィックとストレージを消費する可能性があります + IPアドレスのブロックを回避することができます + 保存されたマンガの処理 + ダウンロードの速度低下 + 並列ダウンロード + チャプターはバックグラウンドで削除されます。時間がかかる場合があります + 隠す \ No newline at end of file From de8739f1439c2dbda3c7caac2b8fde35688d1763 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sun, 1 May 2022 02:12:04 +0200 Subject: [PATCH 09/15] Translated using Weblate (Finnish) Currently translated at 99.6% (279 of 280 strings) Translated using Weblate (French) Currently translated at 100.0% (280 of 280 strings) Translated using Weblate (Italian) Currently translated at 100.0% (280 of 280 strings) Translated using Weblate (German) Currently translated at 100.0% (280 of 280 strings) Translated using Weblate (Finnish) Currently translated at 99.6% (278 of 279 strings) Translated using Weblate (French) Currently translated at 100.0% (279 of 279 strings) Translated using Weblate (Italian) Currently translated at 100.0% (279 of 279 strings) Translated using Weblate (German) Currently translated at 100.0% (279 of 279 strings) Co-authored-by: J. Lavoie Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ Translation: Kotatsu/Strings --- app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values-fi/strings.xml | 7 +++++++ app/src/main/res/values-fr/strings.xml | 6 ++++++ app/src/main/res/values-it/strings.xml | 6 ++++++ 4 files changed, 23 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8933fe241..42fb3a9ee 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -274,4 +274,8 @@ Entfernung abgeschlossen Download-Verzögerung Parallele Downloads + Gespeicherte Manga-Verarbeitung + Hilft, das Blockieren Ihrer IP-Adresse zu vermeiden + Die Kapitel werden im Hintergrund entfernt. Das kann einige Zeit dauern + Ausblenden \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index c6cc64414..69f9c193e 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -271,4 +271,11 @@ Sulje pois genrejä Poista valitut kohteet laitteesta pysyvästi\? Poisto valmis + Rinnakkaislataukset + Latauksen hidastuminen + Auttaa välttämään IP-osoitteesi estämisen + Luvut poistetaan taustalla. Se voi kestää jonkin aikaa + Oletko varma, että haluat ladata kaikki valitut mangat kaikkine lukuineen\? Tämä toiminto voi kuluttaa paljon liikennettä ja tallennustilaa + Tallennettujen mangojen käsittely + Piilota \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 08206fec0..958794036 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -272,4 +272,10 @@ Supprimer définitivement les éléments sélectionnés de l\'appareil \? Suppression terminée Voulez-vous vraiment télécharger tous les mangas sélectionnés avec tous leurs chapitres \? Cette action peut consommer beaucoup de trafic et de stockage + Téléchargements parallèles + Ralentissement du téléchargement + Permet d\'éviter le blocage de votre adresse IP + Les chapitres seront supprimés en arrière-plan. Cela peut prendre un certain temps + Traitement des mangas sauvegardés + Masquer \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7c508afc2..e6196111b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -272,4 +272,10 @@ Rimozione completata Eliminare gli elementi selezionati dal dispositivo in modo permanente\? Vuoi davvero scaricare tutti i manga selezionati con tutti i loro capitoli\? Questa azione può consumare molto traffico e memoria + Scaricamenti paralleli + Rallentamento dello scaricamento + Elaborazione dei manga salvati + I capitoli saranno rimossi in sfondo. Può richiedere un po\' di tempo + Aiuta ad evitare il blocco del tuo indirizzo IP + Nascondi \ No newline at end of file From fa1f2cbf511da79c7b4800b0c731e81e1f9c69d5 Mon Sep 17 00:00:00 2001 From: Luna Jernberg Date: Sun, 1 May 2022 02:12:04 +0200 Subject: [PATCH 10/15] Translated using Weblate (Swedish) Currently translated at 98.5% (275 of 279 strings) Co-authored-by: Luna Jernberg Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/ Translation: Kotatsu/Strings --- app/src/main/res/values-sv/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f477537f4..76cfa061a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -272,4 +272,5 @@ Vill du ta bort markerade objekt från enheten permanent\? %1$s%% Är du säker på att du vill ladda ner alla utvalda manga med alla kapitel\? Den här åtgärden kan kräva mycket nätverkstrafik och lagringsutrymme + Parallella nedladdningar \ No newline at end of file From 59c2d20311bbc4fda87128acfd52cdd5dda642ab Mon Sep 17 00:00:00 2001 From: Luiz-bro Date: Sun, 1 May 2022 02:12:05 +0200 Subject: [PATCH 11/15] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (280 of 280 strings) Co-authored-by: Luiz-bro Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/ Translation: Kotatsu/Strings --- app/src/main/res/values-pt-rBR/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 5097c09fe..32910e0a3 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -272,4 +272,10 @@ Remoção concluída Excluir itens selecionados do dispositivo permanentemente\? Tem certeza de que deseja baixar todos os mangás selecionados com todos os seus capítulos\? Essa ação pode consumir muito tráfego e armazenamento + Esconder + Baixar lentidão + Ajuda a evitar o bloqueio do seu endereço IP + Processamento de mangá salvo + Os capítulos serão removidos em segundo plano. Pode levar algum tempo + Downloads paralelos \ No newline at end of file From 558c19e5265f2129f28ac3ee44f3c30dcfc811c6 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 1 May 2022 09:24:45 +0300 Subject: [PATCH 12/15] Update parsers and version --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 749ed677d..30058f76f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 404 - versionName '3.2' + versionCode 405 + versionName '3.2.1' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -65,7 +65,7 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:72cd6fbadf') { + implementation('com.github.nv95:kotatsu-parsers:d704815acf') { exclude group: 'org.json', module: 'json' } From 28b556121b856896eb595f94a1a7f1f4b98a873c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 2 May 2022 09:35:21 +0300 Subject: [PATCH 13/15] Show new sources on app startup --- .../kotatsu/core/prefs/AppSettings.kt | 14 ++++ .../koitharu/kotatsu/main/ui/MainActivity.kt | 9 ++- .../kotatsu/settings/SettingsModule.kt | 2 + .../newsources/NewSourcesDialogFragment.kt | 68 +++++++++++++++++++ .../newsources/NewSourcesViewModel.kt | 42 ++++++++++++ .../settings/onboard/OnboardDialogFragment.kt | 1 + .../sources/SourcesSettingsFragment.kt | 5 +- .../adapter/SourceConfigAdapterDelegates.kt | 4 +- app/src/main/res/layout/dialog_onboard.xml | 5 +- app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 1 + 11 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 2b65c60a3..d9178713c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -134,6 +134,20 @@ class AppSettings(context: Context) { val isSourcesSelected: Boolean get() = KEY_SOURCES_HIDDEN in prefs + val newSources: Set + get() { + val known = sourcesOrder.toSet() + val hidden = hiddenSources + return remoteMangaSources + .filterNotTo(EnumSet.noneOf(MangaSource::class.java)) { x -> + x.name in known || x.name in hidden + } + } + + fun markKnownSources(sources: Collection) { + sourcesOrder = sourcesOrder + sources.map { it.name } + } + val isPagesNumbersEnabled: Boolean get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index d1e033cb7..24805aa84 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -49,6 +49,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker @@ -390,10 +391,14 @@ class MainActivity : if (AppUpdateChecker.isUpdateSupported(this@MainActivity)) { AppUpdateChecker(this@MainActivity).checkIfNeeded() } - if (!get().isSourcesSelected) { - withContext(Dispatchers.Main) { + val settings = get() + when { + !settings.isSourcesSelected -> withContext(Dispatchers.Main) { OnboardDialogFragment.showWelcome(supportFragmentManager) } + settings.newSources.isNotEmpty() -> withContext(Dispatchers.Main) { + NewSourcesDialogFragment.show(supportFragmentManager) + } } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt index e8e7f97ae..b1fd14c50 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.backup.RestoreRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.backup.BackupViewModel import org.koitharu.kotatsu.settings.backup.RestoreViewModel +import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel import org.koitharu.kotatsu.settings.onboard.OnboardViewModel import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel @@ -27,4 +28,5 @@ val settingsModule viewModel { ProtectSetupViewModel(get()) } viewModel { OnboardViewModel(get()) } viewModel { SourcesSettingsViewModel(get()) } + viewModel { NewSourcesViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt new file mode 100644 index 000000000..9718e5306 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt @@ -0,0 +1,68 @@ +package org.koitharu.kotatsu.settings.newsources + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.koin.android.ext.android.get +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.databinding.DialogOnboardBinding +import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter +import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener +import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem + +class NewSourcesDialogFragment : + AlertDialogFragment(), + SourceConfigListener, + DialogInterface.OnClickListener { + + private val viewModel by viewModel() + + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogOnboardBinding { + return DialogOnboardBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val adapter = SourceConfigAdapter(this, get(), viewLifecycleOwner) + binding.recyclerView.adapter = adapter + binding.textViewTitle.setText(R.string.new_sources_text) + + viewModel.sources.observe(viewLifecycleOwner) { adapter.items = it } + } + + override fun onBuildDialog(builder: MaterialAlertDialogBuilder) { + builder + .setPositiveButton(R.string.done, this) + .setCancelable(true) + .setTitle(R.string.remote_sources) + } + + override fun onClick(dialog: DialogInterface, which: Int) { + viewModel.apply() + dialog.dismiss() + } + + override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) = Unit + + override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { + viewModel.onItemEnabledChanged(item, isEnabled) + } + + override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) = Unit + + override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit + + companion object { + + private const val TAG = "NewSources" + + fun show(fm: FragmentManager) = NewSourcesDialogFragment().show(fm, TAG) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt new file mode 100644 index 000000000..530851d46 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.settings.newsources + +import androidx.lifecycle.MutableLiveData +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem + +class NewSourcesViewModel( + private val settings: AppSettings, +) : BaseViewModel() { + + val sources = MutableLiveData>() + private val initialList = settings.newSources + + init { + buildList() + } + + fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { + if (isEnabled) { + settings.hiddenSources -= item.source.name + } else { + settings.hiddenSources += item.source.name + } + } + + fun apply() { + settings.markKnownSources(initialList) + } + + private fun buildList() { + val hidden = settings.hiddenSources + sources.value = initialList.map { + SourceConfigItem.SourceItem( + source = it, + summary = null, + isEnabled = it.name !in hidden, + isDraggable = false, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt index 005469b96..92fee1db9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt @@ -53,6 +53,7 @@ class OnboardDialogFragment : AlertDialogFragment(), super.onViewCreated(view, savedInstanceState) val adapter = SourceLocalesAdapter(this) binding.recyclerView.adapter = adapter + binding.textViewTitle.setText(R.string.onboard_text) viewModel.list.observeNotNull(viewLifecycleOwner) { adapter.items = it } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index ad27cec65..60a5b6ff9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -141,7 +141,10 @@ class SourcesSettingsFragment : BaseFragment(), recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder, - ): Boolean = current.itemViewType == target.itemViewType + ): Boolean = current.itemViewType == target.itemViewType && viewModel.canReorder( + current.bindingAdapterPosition, + target.bindingAdapterPosition, + ) override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt index 48f0e74c5..752e3d33e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt @@ -83,7 +83,9 @@ fun sourceConfigDraggableItemDelegate( on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable } ) { - val eventListener = object : View.OnClickListener, View.OnTouchListener, + val eventListener = object : + View.OnClickListener, + View.OnTouchListener, CompoundButton.OnCheckedChangeListener { override fun onClick(v: View?) = listener.onItemSettingsClick(item) diff --git a/app/src/main/res/layout/dialog_onboard.xml b/app/src/main/res/layout/dialog_onboard.xml index 03346c04d..e9757cc61 100644 --- a/app/src/main/res/layout/dialog_onboard.xml +++ b/app/src/main/res/layout/dialog_onboard.xml @@ -8,14 +8,15 @@ android:orientation="vertical"> + android:textAppearance="?attr/textAppearanceBodyMedium" + tools:text="@string/onboard_text" /> Помогает избежать блокировки IP-адреса Обработка сохранённой манги Главы будут удалены в фоновом режиме. Это может занять какое-то время + Скрыть + Доступны новые источники манги \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cca5b5410..692597cee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -280,4 +280,5 @@ Saved manga processing Chapters will be removed in the background. It can take some time Hide + New manga sources are available \ No newline at end of file From 96be49aa83cabc7f91311e968184f6e45a255c56 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 2 May 2022 09:38:48 +0300 Subject: [PATCH 14/15] Update monochrome launcher icon --- .../res/drawable/ic_launcher_monochrome.xml | 22 +++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_launcher_monochrome.xml diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 000000000..1048bdfff --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index df1ae1085..4c1843bb3 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -3,5 +3,5 @@ xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 811595fcf..0ed32d5be 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file From 4c5314fe59865359693083c09158b783b9ad71a4 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 2 May 2022 14:53:31 +0300 Subject: [PATCH 15/15] Update parsers --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 30058f76f..ff599eb34 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,7 +65,7 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:d704815acf') { + implementation('com.github.nv95:kotatsu-parsers:090ad4b256') { exclude group: 'org.json', module: 'json' }