Merge branch 'devel' into feature/nextgen

pull/163/head
Koitharu 4 years ago
commit e4a2897731
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@ -24,7 +25,6 @@ import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.SettingsActivity
abstract class BaseActivity<B : ViewBinding> : abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(), AppCompatActivity(),
@ -83,10 +83,8 @@ abstract class BaseActivity<B : ViewBinding> :
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
// ActivityCompat.recreate(this) ActivityCompat.recreate(this)
// throw RuntimeException("Test crash") return true
startActivity(SettingsActivity.newIntent(this)) // TODO Xtimms REMOVE
// return true
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }

@ -0,0 +1,162 @@
package org.koitharu.kotatsu.base.ui.list
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import kotlin.coroutines.EmptyCoroutineContext
private const val KEY_SELECTION = "selection"
private const val PROVIDER_NAME = "selection_decoration"
class ListSelectionController(
private val activity: Activity,
private val decoration: AbstractSelectionItemDecoration,
private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback,
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
private var actionMode: ActionMode? = null
private val stateEventObserver = StateEventObserver()
val count: Int
get() = decoration.checkedItemsCount
fun snapshot(): Set<Long> {
return peekCheckedIds().toSet()
}
fun peekCheckedIds(): Set<Long> {
return decoration.checkedItemsIds
}
fun clear() {
decoration.clearSelection()
notifySelectionChanged()
}
fun addAll(ids: Collection<Long>) {
if (ids.isEmpty()) {
return
}
decoration.checkAll(ids)
notifySelectionChanged()
}
fun attachToRecyclerView(recyclerView: RecyclerView) {
recyclerView.addItemDecoration(decoration)
registryOwner.lifecycle.addObserver(stateEventObserver)
}
override fun saveState(): Bundle {
val bundle = Bundle(1)
bundle.putLongArray(KEY_SELECTION, peekCheckedIds().toLongArray())
return bundle
}
fun onItemClick(id: Long): Boolean {
if (decoration.checkedItemsCount != 0) {
decoration.toggleItemChecked(id)
if (decoration.checkedItemsCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
notifySelectionChanged()
return true
}
return false
}
fun onItemLongClick(id: Long): Boolean {
startActionMode()
return actionMode?.also {
decoration.setItemIsChecked(id, true)
notifySelectionChanged()
} != null
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onPrepareActionMode(mode, menu)
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return callback.onActionItemClicked(mode, item)
}
override fun onDestroyActionMode(mode: ActionMode) {
callback.onDestroyActionMode(mode)
clear()
actionMode = null
}
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
}
private fun notifySelectionChanged() {
val count = decoration.checkedItemsCount
callback.onSelectionChanged(count)
if (count == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
}
private fun restoreState(ids: Collection<Long>) {
if (ids.isEmpty() || decoration.checkedItemsCount != 0) {
return
}
decoration.checkAll(ids)
startActionMode()
notifySelectionChanged()
}
interface Callback : ActionMode.Callback {
fun onSelectionChanged(count: Int)
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean
override fun onDestroyActionMode(mode: ActionMode) = Unit
}
private inner class StateEventObserver : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_CREATE) {
val registry = registryOwner.savedStateRegistry
registry.registerSavedStateProvider(PROVIDER_NAME, this@ListSelectionController)
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
if (state != null) {
Dispatchers.Main.dispatch(EmptyCoroutineContext) { // == Handler.post
if (source.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
restoreState(state.getLongArray(KEY_SELECTION)?.toList().orEmpty())
}
}
}
}
}
}
}

@ -29,6 +29,9 @@ val uiModule
ImageLoader.Builder(androidContext()) ImageLoader.Builder(androidContext())
.okHttpClient(httpClientFactory) .okHttpClient(httpClientFactory)
.interceptorDispatcher(Dispatchers.Default) .interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.IO)
.decoderDispatcher(Dispatchers.Default)
.transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory) .diskCache(diskCacheFactory)
.components( .components(
ComponentRegistry.Builder() ComponentRegistry.Builder()

@ -5,7 +5,6 @@ import android.os.Bundle
import android.view.* import android.view.*
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.Spinner import android.widget.Spinner
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
@ -16,6 +15,7 @@ import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
@ -34,16 +34,15 @@ import kotlin.math.roundToInt
class ChaptersFragment : class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(), BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem>, OnListItemClickListener<ChapterListItem>,
ActionMode.Callback,
AdapterView.OnItemSelectedListener, AdapterView.OnItemSelectedListener,
MenuItem.OnActionExpandListener, MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener { SearchView.OnQueryTextListener,
ListSelectionController.Callback {
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by sharedViewModel<DetailsViewModel>()
private var chaptersAdapter: ChaptersAdapter? = null private var chaptersAdapter: ChaptersAdapter? = null
private var actionMode: ActionMode? = null private var selectionController: ListSelectionController? = null
private var selectionDecoration: ChaptersSelectionDecoration? = null
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -53,9 +52,14 @@ class ChaptersFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
chaptersAdapter = ChaptersAdapter(this) chaptersAdapter = ChaptersAdapter(this)
selectionDecoration = ChaptersSelectionDecoration(view.context) selectionController = ListSelectionController(
activity = requireActivity(),
decoration = ChaptersSelectionDecoration(view.context),
registryOwner = this,
callback = this,
)
with(binding.recyclerViewChapters) { with(binding.recyclerViewChapters) {
addItemDecoration(selectionDecoration!!) checkNotNull(selectionController).attachToRecyclerView(this)
setHasFixedSize(true) setHasFixedSize(true)
adapter = chaptersAdapter adapter = chaptersAdapter
} }
@ -74,20 +78,13 @@ class ChaptersFragment :
override fun onDestroyView() { override fun onDestroyView() {
chaptersAdapter = null chaptersAdapter = null
selectionDecoration = null selectionController = null
binding.spinnerBranches?.adapter = null binding.spinnerBranches?.adapter = null
super.onDestroyView() super.onDestroyView()
} }
override fun onItemClick(item: ChapterListItem, view: View) { override fun onItemClick(item: ChapterListItem, view: View) {
if (selectionDecoration?.checkedItemsCount != 0) { if (selectionController?.onItemClick(item.chapter.id) == true) {
selectionDecoration?.toggleItemChecked(item.chapter.id)
if (selectionDecoration?.checkedItemsCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
binding.recyclerViewChapters.invalidateItemDecorations()
}
return return
} }
if (item.hasFlag(ChapterListItem.FLAG_MISSING)) { if (item.hasFlag(ChapterListItem.FLAG_MISSING)) {
@ -106,14 +103,7 @@ class ChaptersFragment :
} }
override fun onItemLongClick(item: ChapterListItem, view: View): Boolean { override fun onItemLongClick(item: ChapterListItem, view: View): Boolean {
if (actionMode == null) { return selectionController?.onItemLongClick(item.chapter.id) ?: false
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
return actionMode?.also {
selectionDecoration?.setItemIsChecked(item.chapter.id, true)
binding.recyclerViewChapters.invalidateItemDecorations()
it.invalidate()
} != null
} }
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
@ -122,13 +112,13 @@ class ChaptersFragment :
DownloadService.start( DownloadService.start(
context ?: return false, context ?: return false,
viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false, viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false,
selectionDecoration?.checkedItemsIds?.toSet() selectionController?.snapshot(),
) )
mode.finish() mode.finish()
true true
} }
R.id.action_delete -> { R.id.action_delete -> {
val ids = selectionDecoration?.checkedItemsIds val ids = selectionController?.peekCheckedIds()
val manga = viewModel.manga.value val manga = viewModel.manga.value
when { when {
ids.isNullOrEmpty() || manga == null -> Unit ids.isNullOrEmpty() || manga == null -> Unit
@ -147,9 +137,7 @@ class ChaptersFragment :
} }
R.id.action_select_all -> { R.id.action_select_all -> {
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
selectionDecoration?.checkAll(ids) selectionController?.addAll(ids)
binding.recyclerViewChapters.invalidateItemDecorations()
mode.invalidate()
true true
} }
else -> false else -> false
@ -169,7 +157,7 @@ class ChaptersFragment :
} }
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val selectedIds = selectionDecoration?.checkedItemsIds ?: return false val selectedIds = selectionController?.peekCheckedIds() ?: return false
val items = chaptersAdapter?.items?.filter { x -> x.chapter.id in selectedIds }.orEmpty() val items = chaptersAdapter?.items?.filter { x -> x.chapter.id in selectedIds }.orEmpty()
menu.findItem(R.id.action_save).isVisible = items.none { x -> menu.findItem(R.id.action_save).isVisible = items.none { x ->
x.chapter.source == MangaSource.LOCAL x.chapter.source == MangaSource.LOCAL
@ -181,10 +169,8 @@ class ChaptersFragment :
return true return true
} }
override fun onDestroyActionMode(mode: ActionMode?) { override fun onSelectionChanged(count: Int) {
selectionDecoration?.clearSelection()
binding.recyclerViewChapters.invalidateItemDecorations() binding.recyclerViewChapters.invalidateItemDecorations()
actionMode = null
} }
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
@ -18,6 +17,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager
import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
@ -46,12 +46,11 @@ abstract class MangaListFragment :
PaginationScrollListener.Callback, PaginationScrollListener.Callback,
MangaListListener, MangaListListener,
SwipeRefreshLayout.OnRefreshListener, SwipeRefreshLayout.OnRefreshListener,
ActionMode.Callback { ListSelectionController.Callback {
private var listAdapter: MangaListAdapter? = null private var listAdapter: MangaListAdapter? = null
private var paginationListener: PaginationScrollListener? = null private var paginationListener: PaginationScrollListener? = null
private var selectionDecoration: MangaSelectionDecoration? = null private var selectionController: ListSelectionController? = null
private var actionMode: ActionMode? = null
private val spanResolver = MangaListSpanResolver() private val spanResolver = MangaListSpanResolver()
private val spanSizeLookup = SpanSizeLookup() private val spanSizeLookup = SpanSizeLookup()
private val listCommitCallback = Runnable { private val listCommitCallback = Runnable {
@ -62,7 +61,7 @@ abstract class MangaListFragment :
protected abstract val viewModel: MangaListViewModel protected abstract val viewModel: MangaListViewModel
protected val selectedItemsIds: Set<Long> protected val selectedItemsIds: Set<Long>
get() = selectionDecoration?.checkedItemsIds?.toSet().orEmpty() get() = selectionController?.snapshot().orEmpty()
protected val selectedItems: Set<Manga> protected val selectedItems: Set<Manga>
get() = collectSelectedItems() get() = collectSelectedItems()
@ -79,12 +78,17 @@ abstract class MangaListFragment :
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
listener = this, listener = this,
) )
selectionDecoration = MangaSelectionDecoration(view.context) selectionController = ListSelectionController(
activity = requireActivity(),
decoration = MangaSelectionDecoration(view.context),
registryOwner = this,
callback = this,
)
paginationListener = PaginationScrollListener(4, this) paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
adapter = listAdapter adapter = listAdapter
addItemDecoration(selectionDecoration!!) checkNotNull(selectionController).attachToRecyclerView(binding.recyclerView)
addOnScrollListener(paginationListener!!) addOnScrollListener(paginationListener!!)
} }
with(binding.swipeRefreshLayout) { with(binding.swipeRefreshLayout) {
@ -105,34 +109,19 @@ abstract class MangaListFragment :
override fun onDestroyView() { override fun onDestroyView() {
listAdapter = null listAdapter = null
paginationListener = null paginationListener = null
selectionDecoration = null selectionController = null
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()
super.onDestroyView() super.onDestroyView()
} }
override fun onItemClick(item: Manga, view: View) { override fun onItemClick(item: Manga, view: View) {
if (selectionDecoration?.checkedItemsCount != 0) { if (selectionController?.onItemClick(item.id) != true) {
selectionDecoration?.toggleItemChecked(item.id) startActivity(DetailsActivity.newIntent(context ?: return, item))
if (selectionDecoration?.checkedItemsCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
binding.recyclerView.invalidateItemDecorations()
}
return
} }
startActivity(DetailsActivity.newIntent(context ?: return, item))
} }
override fun onItemLongClick(item: Manga, view: View): Boolean { override fun onItemLongClick(item: Manga, view: View): Boolean {
if (actionMode == null) { return selectionController?.onItemLongClick(item.id) ?: false
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
return actionMode?.also {
selectionDecoration?.setItemIsChecked(item.id, true)
binding.recyclerView.invalidateItemDecorations()
it.invalidate()
} != null
} }
@CallSuper @CallSuper
@ -245,7 +234,7 @@ abstract class MangaListFragment :
addOnLayoutChangeListener(spanResolver) addOnLayoutChangeListener(spanResolver)
} }
} }
selectionDecoration?.let { addItemDecoration(it) } selectionController?.attachToRecyclerView(binding.recyclerView)
} }
} }
@ -255,7 +244,7 @@ abstract class MangaListFragment :
@CallSuper @CallSuper
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.title = selectionDecoration?.checkedItemsCount?.toString() mode.title = selectionController?.count?.toString()
return true return true
} }
@ -265,9 +254,7 @@ abstract class MangaListFragment :
val ids = listAdapter?.items?.mapNotNull { val ids = listAdapter?.items?.mapNotNull {
(it as? MangaItemModel)?.id (it as? MangaItemModel)?.id
} ?: return false } ?: return false
selectionDecoration?.checkAll(ids) selectionController?.addAll(ids)
binding.recyclerView.invalidateItemDecorations()
mode.invalidate()
true true
} }
R.id.action_share -> { R.id.action_share -> {
@ -289,14 +276,12 @@ abstract class MangaListFragment :
} }
} }
override fun onDestroyActionMode(mode: ActionMode) { override fun onSelectionChanged(count: Int) {
selectionDecoration?.clearSelection()
binding.recyclerView.invalidateItemDecorations() binding.recyclerView.invalidateItemDecorations()
actionMode = null
} }
private fun collectSelectedItems(): Set<Manga> { private fun collectSelectedItems(): Set<Manga> {
val checkedIds = selectionDecoration?.checkedItemsIds ?: return emptySet() val checkedIds = selectionController?.peekCheckedIds() ?: return emptySet()
val items = listAdapter?.items ?: return emptySet() val items = listAdapter?.items ?: return emptySet()
val result = ArraySet<Manga>(checkedIds.size) val result = ArraySet<Manga>(checkedIds.size)
for (item in items) { for (item in items) {

@ -107,6 +107,17 @@ class MainActivity :
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged) viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
} }
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
if (isSearchOpened()) {
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = SCROLL_FLAG_NO_SCROLL
}
binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant))
binding.appbar.updatePadding(left = 0, right = 0)
}
}
override fun onBackPressed() { override fun onBackPressed() {
val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH)
binding.searchView.clearFocus() binding.searchView.clearFocus()
@ -291,6 +302,10 @@ class MainActivity :
binding.navRail?.isVisible = visible binding.navRail?.isVisible = visible
} }
private fun isSearchOpened(): Boolean {
return supportFragmentManager.findFragmentByTag(TAG_SEARCH)?.isVisible == true
}
private fun onFirstStart() { private fun onFirstStart() {
lifecycleScope.launchWhenResumed { lifecycleScope.launchWhenResumed {
val isUpdateSupported = withContext(Dispatchers.Default) { val isUpdateSupported = withContext(Dispatchers.Default) {
@ -312,7 +327,7 @@ class MainActivity :
private fun adjustFabVisibility( private fun adjustFabVisibility(
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true, isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true,
topFragment: Fragment? = supportFragmentManager.findFragmentByTag(TAG_PRIMARY), topFragment: Fragment? = supportFragmentManager.findFragmentByTag(TAG_PRIMARY),
isSearchOpened: Boolean = supportFragmentManager.findFragmentByTag(TAG_SEARCH)?.isVisible == true, isSearchOpened: Boolean = isSearchOpened(),
) { ) {
val fab = binding.fab val fab = binding.fab
if (isResumeEnabled && !isSearchOpened && topFragment is LibraryFragment) { if (isResumeEnabled && !isSearchOpened && topFragment is LibraryFragment) {

@ -17,6 +17,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
@ -32,14 +33,14 @@ import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.findViewsByType import org.koitharu.kotatsu.utils.ext.findViewsByType
class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaListListener, ActionMode.Callback { class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaListListener,
ListSelectionController.Callback {
private val viewModel by viewModel<MultiSearchViewModel> { private val viewModel by viewModel<MultiSearchViewModel> {
parametersOf(intent.getStringExtra(EXTRA_QUERY).orEmpty()) parametersOf(intent.getStringExtra(EXTRA_QUERY).orEmpty())
} }
private lateinit var adapter: MultiSearchAdapter private lateinit var adapter: MultiSearchAdapter
private lateinit var selectionDecoration: MangaSelectionDecoration private lateinit var selectionController: ListSelectionController
private var actionMode: ActionMode? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -51,7 +52,13 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
} }
} }
val sizeResolver = ItemSizeResolver(resources, get()) val sizeResolver = ItemSizeResolver(resources, get())
selectionDecoration = MangaSelectionDecoration(this) val selectionDecoration = MangaSelectionDecoration(this)
selectionController = ListSelectionController(
activity = this,
decoration = selectionDecoration,
registryOwner = this,
callback = this,
)
adapter = MultiSearchAdapter( adapter = MultiSearchAdapter(
lifecycleOwner = this, lifecycleOwner = this,
coil = get(), coil = get(),
@ -90,29 +97,14 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
} }
override fun onItemClick(item: Manga, view: View) { override fun onItemClick(item: Manga, view: View) {
if (selectionDecoration.checkedItemsCount != 0) { if (!selectionController.onItemClick(item.id)) {
selectionDecoration.toggleItemChecked(item.id) val intent = DetailsActivity.newIntent(this, item)
if (selectionDecoration.checkedItemsCount == 0) { startActivity(intent)
actionMode?.finish()
} else {
actionMode?.invalidate()
invalidateItemDecorations()
}
return
} }
val intent = DetailsActivity.newIntent(this, item)
startActivity(intent)
} }
override fun onItemLongClick(item: Manga, view: View): Boolean { override fun onItemLongClick(item: Manga, view: View): Boolean {
if (actionMode == null) { return selectionController.onItemLongClick(item.id)
actionMode = startSupportActionMode(this)
}
return actionMode?.also {
selectionDecoration.setItemIsChecked(item.id, true)
invalidateItemDecorations()
it.invalidate()
} != null
} }
override fun onRetryClick(error: Throwable) { override fun onRetryClick(error: Throwable) {
@ -131,7 +123,7 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
} }
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.title = selectionDecoration.checkedItemsCount.toString() mode.title = selectionController.count.toString()
return true return true
} }
@ -156,22 +148,16 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
} }
} }
override fun onDestroyActionMode(mode: ActionMode) { override fun onSelectionChanged(count: Int) {
selectionDecoration.clearSelection()
invalidateItemDecorations()
actionMode = null
}
private fun collectSelectedItems(): Set<Manga> {
return viewModel.getItems(selectionDecoration.checkedItemsIds)
}
private fun invalidateItemDecorations() {
binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach { binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach {
it.invalidateItemDecorations() it.invalidateItemDecorations()
} }
} }
private fun collectSelectedItems(): Set<Manga> {
return viewModel.getItems(selectionController.peekCheckedIds())
}
companion object { companion object {
private const val EXTRA_QUERY = "query" private const val EXTRA_QUERY = "query"

@ -6,6 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
class MultiSearchListModel( class MultiSearchListModel(
val source: MangaSource, val source: MangaSource,
val hasMore: Boolean,
val list: List<MangaItemModel>, val list: List<MangaItemModel>,
) : ListModel { ) : ListModel {
@ -16,6 +17,7 @@ class MultiSearchListModel(
other as MultiSearchListModel other as MultiSearchListModel
if (source != other.source) return false if (source != other.source) return false
if (hasMore != other.hasMore) return false
if (list != other.list) return false if (list != other.list) return false
return true return true
@ -23,6 +25,7 @@ class MultiSearchListModel(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = source.hashCode() var result = source.hashCode()
result = 31 * result + hasMore.hashCode()
result = 31 * result + list.hashCode() result = 31 * result + list.hashCode()
return result return result
} }

@ -19,6 +19,7 @@ import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
private const val MAX_PARALLELISM = 4 private const val MAX_PARALLELISM = 4
private const val MIN_HAS_MORE_ITEMS = 8
class MultiSearchViewModel( class MultiSearchViewModel(
initialQuery: String, initialQuery: String,
@ -98,7 +99,7 @@ class MultiSearchViewModel(
val list = MangaRepository(source).getList(offset = 0, query = q) val list = MangaRepository(source).getList(offset = 0, query = q)
.toUi(ListMode.GRID) .toUi(ListMode.GRID)
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
MultiSearchListModel(source, list) MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list)
} else { } else {
null null
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.search.ui.multi.adapter package org.koitharu.kotatsu.search.ui.multi.adapter
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import coil.ImageLoader import coil.ImageLoader
@ -38,11 +39,12 @@ fun searchResultsAD(
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing) val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing)
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing)) binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener) val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener)
itemView.setOnClickListener(eventListener) binding.buttonMore.setOnClickListener(eventListener)
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title
adapter.items = item.list binding.buttonMore.isVisible = item.hasMore
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
adapter.items = item.list
} }
} }

@ -0,0 +1,96 @@
package org.koitharu.kotatsu.utils.image
import android.graphics.Bitmap
import androidx.core.graphics.get
import coil.size.Size
import coil.transform.Transformation
class TrimTransformation : Transformation {
override val cacheKey: String = javaClass.name
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
var left = 0
var top = 0
var right = 0
var bottom = 0
// Left
for (x in 0 until input.width) {
var isColBlank = true
val prevColor = input[x, 0]
for (y in 1 until input.height) {
if (input[x, y] != prevColor) {
isColBlank = false
break
}
}
if (isColBlank) {
left++
} else {
break
}
}
if (left == input.width) {
return input
}
// Right
for (x in (left until input.width).reversed()) {
var isColBlank = true
val prevColor = input[x, 0]
for (y in 1 until input.height) {
if (input[x, y] != prevColor) {
isColBlank = false
break
}
}
if (isColBlank) {
right++
} else {
break
}
}
// Top
for (y in 0 until input.height) {
var isRowBlank = true
val prevColor = input[0, y]
for (x in 1 until input.width) {
if (input[x, y] != prevColor) {
isRowBlank = false
break
}
}
if (isRowBlank) {
top++
} else {
break
}
}
// Bottom
for (y in (top until input.height).reversed()) {
var isRowBlank = true
val prevColor = input[0, y]
for (x in 1 until input.width) {
if (input[x, y] != prevColor) {
isRowBlank = false
break
}
}
if (isRowBlank) {
bottom++
} else {
break
}
}
return if (left != 0 || right != 0 || top != 0 || bottom != 0) {
Bitmap.createBitmap(input, left, top, input.width - left - right, input.height - top - bottom)
} else {
input
}
}
override fun equals(other: Any?) = other is TrimTransformation
override fun hashCode() = javaClass.hashCode()
}

@ -1,32 +1,49 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:clipChildren="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingVertical="@dimen/grid_spacing_outer"> android:paddingBottom="@dimen/grid_spacing_outer">
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginHorizontal="@dimen/grid_spacing" android:layout_marginHorizontal="@dimen/grid_spacing"
android:layout_marginTop="@dimen/grid_spacing_outer"
android:layout_toStartOf="@id/button_more"
android:gravity="center_vertical|start" android:gravity="center_vertical|start"
android:padding="@dimen/grid_spacing" android:padding="@dimen/grid_spacing"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
tools:text="@tools:sample/lorem[2]" /> tools:text="@tools:sample/lorem[2]" />
<Button
android:id="@+id/button_more"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/textView_title"
android:layout_alignParentEnd="true"
android:layout_marginEnd="@dimen/grid_spacing"
android:text="@string/show_all" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/textView_title"
android:layout_alignParentStart="true"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="@dimen/grid_spacing" android:paddingHorizontal="@dimen/grid_spacing"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout> </RelativeLayout>

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.CardView.Outlined" style="@style/Widget.Material3.CardView.Outlined"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
app:contentPadding="4dp" app:contentPadding="4dp"
app:shapeAppearance="?shapeAppearanceCornerSmall"
tools:layout_height="@dimen/search_suggestions_manga_height"> tools:layout_height="@dimen/search_suggestions_manga_height">
<LinearLayout <LinearLayout
@ -19,9 +20,9 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
android:orientation="vertical" android:orientation="vertical"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<TextView <TextView

@ -319,4 +319,5 @@
<string name="show_reading_indicators_summary">Show percentage read in history and favourites</string> <string name="show_reading_indicators_summary">Show percentage read in history and favourites</string>
<string name="exclude_nsfw_from_history_summary">Manga marked as NSFW will never added to the history and your progress will not be saved</string> <string name="exclude_nsfw_from_history_summary">Manga marked as NSFW will never added to the history and your progress will not be saved</string>
<string name="clear_cookies_summary">Can help in case of some issues. All authorizations will be invalidated</string> <string name="clear_cookies_summary">Can help in case of some issues. All authorizations will be invalidated</string>
<string name="show_all">Show all</string>
</resources> </resources>
Loading…
Cancel
Save