Rework sources configuration screen

pull/311/head
Koitharu 3 years ago
parent 29114ae8a7
commit 672a1e9b2a
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -0,0 +1,6 @@
package org.koitharu.kotatsu.base.ui.list
interface OnTipCloseListener<T> {
fun onCloseTip(tip: T)
}

@ -0,0 +1,38 @@
package org.koitharu.kotatsu.base.ui.util
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import androidx.lifecycle.Observer
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
class ReversibleActionObserver(
private val snackbarHost: View,
) : Observer<ReversibleAction> {
override fun onChanged(action: ReversibleAction?) {
if (action == null) {
return
}
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make(snackbarHost, action.stringResId, length)
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}
(snackbarHost.context.findActivity() as? BottomNavOwner)?.let {
snackbar.anchorView = it.bottomNav
}
snackbar.show()
}
private fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
}

@ -293,6 +293,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
return list return list
} }
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
}
fun closeTip(tip: String) {
val closedTips = prefs.getStringSet(KEY_TIPS_CLOSED, emptySet()).orEmpty()
if (tip in closedTips) {
return
}
prefs.edit { putStringSet(KEY_TIPS_CLOSED, closedTips + tip) }
}
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) { fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener) prefs.registerOnSharedPreferenceChangeListener(listener)
} }
@ -380,6 +392,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_LOGS_SHARE = "logs_share" const val KEY_LOGS_SHARE = "logs_share"
const val KEY_SOURCES_GRID = "sources_grid" const val KEY_SOURCES_GRID = "sources_grid"
const val KEY_UPDATES_UNSTABLE = "updates_unstable" const val KEY_UPDATES_UNSTABLE = "updates_unstable"
const val KEY_TIPS_CLOSED = "tips_closed"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

@ -16,11 +16,10 @@ import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.base.ui.util.SpanSizeResolver import org.koitharu.kotatsu.base.ui.util.SpanSizeResolver
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
import org.koitharu.kotatsu.databinding.FragmentExploreBinding import org.koitharu.kotatsu.databinding.FragmentExploreBinding
@ -77,7 +76,7 @@ class ExploreFragment :
} }
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga) viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone) viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged) viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
} }
@ -145,17 +144,6 @@ class ExploreFragment :
startActivity(intent) startActivity(intent)
} }
private fun onActionDone(action: ReversibleAction) {
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length)
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}
snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav
snackbar.show()
}
private fun onGridModeChanged(isGrid: Boolean) { private fun onGridModeChanged(isGrid: Boolean) {
binding.recyclerView.layoutManager = if (isGrid) { binding.recyclerView.layoutManager = if (isGrid) {
GridLayoutManager(requireContext(), 4).also { lm -> GridLayoutManager(requireContext(), 4).also { lm ->

@ -7,17 +7,15 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.databinding.DialogOnboardBinding 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.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class NewSourcesDialogFragment : class NewSourcesDialogFragment :
@ -36,7 +34,7 @@ class NewSourcesDialogFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val adapter = SourceConfigAdapter(this, coil, viewLifecycleOwner) val adapter = SourcesSelectAdapter(this, coil, viewLifecycleOwner)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.textViewTitle.setText(R.string.new_sources_text) binding.textViewTitle.setText(R.string.new_sources_text)
@ -61,10 +59,10 @@ class NewSourcesDialogFragment :
viewModel.onItemEnabledChanged(item, isEnabled) viewModel.onItemEnabledChanged(item, isEnabled)
} }
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) = Unit
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
companion object { companion object {
private const val TAG = "NewSources" private const val TAG = "NewSources"

@ -0,0 +1,18 @@
package org.koitharu.kotatsu.settings.newsources
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigDiffCallback
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.adapter.sourceConfigItemCheckableDelegate
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourcesSelectAdapter(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : AsyncListDifferDelegationAdapter<SourceConfigItem>(
SourceConfigDiffCallback(),
sourceConfigItemCheckableDelegate(listener, coil, lifecycleOwner),
)

@ -1,7 +1,12 @@
package org.koitharu.kotatsu.settings.sources package org.koitharu.kotatsu.settings.sources
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
@ -11,10 +16,10 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
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.util.RecyclerViewOwner import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
@ -24,6 +29,8 @@ import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.getItem
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SourcesSettingsFragment : class SourcesSettingsFragment :
@ -63,6 +70,7 @@ class SourcesSettingsFragment :
viewModel.items.observe(viewLifecycleOwner) { viewModel.items.observe(viewLifecycleOwner) {
sourcesAdapter.items = it sourcesAdapter.items = it
} }
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
addMenuProvider(SourcesMenuProvider()) addMenuProvider(SourcesMenuProvider())
} }
@ -89,14 +97,14 @@ class SourcesSettingsFragment :
viewModel.setEnabled(item.source, isEnabled) viewModel.setEnabled(item.source, isEnabled)
} }
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) {
reorderHelper?.startDrag(holder)
}
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) { override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) {
viewModel.expandOrCollapse(header.localeId) viewModel.expandOrCollapse(header.localeId)
} }
override fun onCloseTip(tip: SourceConfigItem.Tip) {
viewModel.onTipClosed(tip)
}
private inner class SourcesMenuProvider : private inner class SourcesMenuProvider :
MenuProvider, MenuProvider,
MenuItem.OnActionExpandListener, MenuItem.OnActionExpandListener,
@ -117,6 +125,7 @@ class SourcesSettingsFragment :
viewModel.disableAll() viewModel.disableAll()
true true
} }
else -> false else -> false
} }
@ -140,7 +149,7 @@ class SourcesSettingsFragment :
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback( private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP, ItemTouchHelper.DOWN or ItemTouchHelper.UP,
0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT,
) { ) {
override fun onMove( override fun onMove(
@ -161,8 +170,31 @@ class SourcesSettingsFragment :
target.bindingAdapterPosition, target.bindingAdapterPosition,
) )
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit override fun getDragDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val item = viewHolder.getItem(SourceConfigItem.SourceItem::class.java)
return if (item != null && item.isDraggable) {
super.getDragDirs(recyclerView, viewHolder)
} else {
0
}
}
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val item = viewHolder.getItem(SourceConfigItem.Tip::class.java)
return if (item != null) {
super.getSwipeDirs(recyclerView, viewHolder)
} else {
0
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val item = viewHolder.getItem(SourceConfigItem.Tip::class.java)
if (item != null) {
viewModel.onTipClosed(item)
}
}
override fun isLongPressDragEnabled() = false override fun isLongPressDragEnabled() = true
} }
} }

@ -3,20 +3,25 @@ package org.koitharu.kotatsu.settings.sources
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.*
import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.map import org.koitharu.kotatsu.utils.ext.map
import org.koitharu.kotatsu.utils.ext.move import org.koitharu.kotatsu.utils.ext.move
import java.util.Locale
import java.util.TreeMap
import javax.inject.Inject
private const val KEY_ENABLED = "!" private const val KEY_ENABLED = "!"
private const val TIP_REORDER = "src_reorder"
@HiltViewModel @HiltViewModel
class SourcesSettingsViewModel @Inject constructor( class SourcesSettingsViewModel @Inject constructor(
@ -24,6 +29,8 @@ class SourcesSettingsViewModel @Inject constructor(
) : BaseViewModel() { ) : BaseViewModel() {
val items = MutableLiveData<List<SourceConfigItem>>(emptyList()) val items = MutableLiveData<List<SourceConfigItem>>(emptyList())
val onActionDone = SingleLiveEvent<ReversibleAction>()
private val expandedGroups = HashSet<String?>() private val expandedGroups = HashSet<String?>()
private var searchQuery: String? = null private var searchQuery: String? = null
@ -58,6 +65,11 @@ class SourcesSettingsViewModel @Inject constructor(
} }
if (isEnabled) { if (isEnabled) {
settings.markKnownSources(setOf(source)) settings.markKnownSources(setOf(source))
} else {
val rollback = ReversibleHandle {
setEnabled(source, true)
}
onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
} }
buildList() buildList()
} }
@ -83,6 +95,11 @@ class SourcesSettingsViewModel @Inject constructor(
buildList() buildList()
} }
fun onTipClosed(item: SourceConfigItem.Tip) {
settings.closeTip(item.key)
buildList()
}
private fun buildList() { private fun buildList() {
val sources = settings.getMangaSources(includeHidden = true) val sources = settings.getMangaSources(includeHidden = true)
val hiddenSources = settings.hiddenSources val hiddenSources = settings.hiddenSources
@ -110,10 +127,13 @@ class SourcesSettingsViewModel @Inject constructor(
it.locale it.locale
} }
} }
val result = ArrayList<SourceConfigItem>(sources.size + map.size + 1) val result = ArrayList<SourceConfigItem>(sources.size + map.size + 2)
val enabledSources = map.remove(KEY_ENABLED) val enabledSources = map.remove(KEY_ENABLED)
if (!enabledSources.isNullOrEmpty()) { if (!enabledSources.isNullOrEmpty()) {
result += SourceConfigItem.Header(R.string.enabled_sources) result += SourceConfigItem.Header(R.string.enabled_sources)
if (settings.isTipEnabled(TIP_REORDER)) {
result += SourceConfigItem.Tip(TIP_REORDER, R.drawable.ic_tap_reorder, R.string.sources_reorder_tip)
}
enabledSources.mapTo(result) { enabledSources.mapTo(result) {
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = it, source = it,

@ -13,7 +13,7 @@ class SourceConfigAdapter(
SourceConfigDiffCallback(), SourceConfigDiffCallback(),
sourceConfigHeaderDelegate(), sourceConfigHeaderDelegate(),
sourceConfigGroupDelegate(listener), sourceConfigGroupDelegate(listener),
sourceConfigItemDelegate(listener, coil, lifecycleOwner), sourceConfigItemDelegate2(listener, coil, lifecycleOwner),
sourceConfigDraggableItemDelegate(listener),
sourceConfigEmptySearchDelegate(), sourceConfigEmptySearchDelegate(),
sourceConfigTipDelegate(listener),
) )

@ -1,21 +1,26 @@
package org.koitharu.kotatsu.settings.sources.adapter package org.koitharu.kotatsu.settings.sources.adapter
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.CompoundButton import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.core.parser.favicon.faviconUri import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.databinding.ItemExpandableBinding import org.koitharu.kotatsu.databinding.ItemExpandableBinding
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding
import org.koitharu.kotatsu.databinding.ItemTipBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.crossfade
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable
fun sourceConfigHeaderDelegate() = fun sourceConfigHeaderDelegate() =
@ -44,13 +49,12 @@ fun sourceConfigGroupDelegate(
} }
} }
fun sourceConfigItemDelegate( fun sourceConfigItemCheckableDelegate(
listener: SourceConfigListener, listener: SourceConfigListener,
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>( ) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent -> ItemSourceConfigCheckableBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable },
) { ) {
binding.switchToggle.setOnCheckedChangeListener { _, isChecked -> binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
@ -77,42 +81,60 @@ fun sourceConfigItemDelegate(
} }
} }
@SuppressLint("ClickableViewAccessibility") fun sourceConfigItemDelegate2(
fun sourceConfigDraggableItemDelegate(
listener: SourceConfigListener, listener: SourceConfigListener,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigDraggableBinding>( coil: ImageLoader,
{ layoutInflater, parent -> ItemSourceConfigDraggableBinding.inflate(layoutInflater, parent, false) }, lifecycleOwner: LifecycleOwner,
on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable }, ) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) },
) { ) {
val eventListener = object : val eventListener = View.OnClickListener { v ->
View.OnClickListener, when (v.id) {
View.OnTouchListener, R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
CompoundButton.OnCheckedChangeListener { R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
override fun onClick(v: View?) = listener.onItemSettingsClick(item) R.id.imageView_config -> listener.onItemSettingsClick(item)
override fun onTouch(v: View?, event: MotionEvent): Boolean {
return if (event.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onDragHandleTouch(this@adapterDelegateViewBinding)
true
} else {
false
} }
} }
binding.imageViewRemove.setOnClickListener(eventListener)
binding.imageViewAdd.setOnClickListener(eventListener)
binding.imageViewConfig.setOnClickListener(eventListener)
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { bind {
listener.onItemEnabledChanged(item, isChecked) binding.textViewTitle.text = item.source.title
binding.imageViewAdd.isGone = item.isEnabled
binding.imageViewRemove.isVisible = item.isEnabled
binding.imageViewConfig.isVisible = item.isEnabled
binding.textViewDescription.textAndVisible = item.summary
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
binding.imageViewIcon.newImageRequest(item.source.faviconUri(), item.source)?.run {
crossfade(context)
error(fallbackIcon)
placeholder(fallbackIcon)
fallback(fallbackIcon)
lifecycle(lifecycleOwner)
enqueueWith(coil)
} }
} }
binding.imageViewConfig.setOnClickListener(eventListener) onViewRecycled {
binding.switchToggle.setOnCheckedChangeListener(eventListener) binding.imageViewIcon.disposeImageRequest()
binding.imageViewHandle.setOnTouchListener(eventListener) }
}
fun sourceConfigTipDelegate(
listener: OnTipCloseListener<SourceConfigItem.Tip>
) = adapterDelegateViewBinding<SourceConfigItem.Tip, SourceConfigItem, ItemTipBinding>(
{ layoutInflater, parent -> ItemTipBinding.inflate(layoutInflater, parent, false) },
) {
binding.buttonClose.setOnClickListener {
listener.onCloseTip(item)
}
bind { bind {
binding.textViewTitle.text = item.source.title binding.imageViewIcon.setImageResource(item.iconResId)
binding.textViewDescription.text = item.summary ?: getString(R.string.various_languages) binding.textView.setText(item.textResId)
binding.switchToggle.isChecked = item.isEnabled
} }
} }

@ -2,7 +2,10 @@ package org.koitharu.kotatsu.settings.sources.adapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.* import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.EmptySearchResult
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.Header
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.LocaleGroup
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.SourceItem
class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() { class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() {
@ -12,15 +15,23 @@ class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() {
oldItem is LocaleGroup && newItem is LocaleGroup -> { oldItem is LocaleGroup && newItem is LocaleGroup -> {
oldItem.localeId == newItem.localeId oldItem.localeId == newItem.localeId
} }
oldItem is SourceItem && newItem is SourceItem -> { oldItem is SourceItem && newItem is SourceItem -> {
oldItem.source == newItem.source oldItem.source == newItem.source
} }
oldItem is Header && newItem is Header -> { oldItem is Header && newItem is Header -> {
oldItem.titleResId == newItem.titleResId oldItem.titleResId == newItem.titleResId
} }
oldItem == EmptySearchResult && newItem == EmptySearchResult -> { oldItem == EmptySearchResult && newItem == EmptySearchResult -> {
true true
} }
oldItem is SourceConfigItem.Tip && newItem is SourceConfigItem.Tip -> {
oldItem.key == newItem.key
}
else -> false else -> false
} }
} }

@ -1,15 +1,13 @@
package org.koitharu.kotatsu.settings.sources.adapter package org.koitharu.kotatsu.settings.sources.adapter
import androidx.recyclerview.widget.RecyclerView import org.koitharu.kotatsu.base.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
interface SourceConfigListener { interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {
fun onItemSettingsClick(item: SourceConfigItem.SourceItem) fun onItemSettingsClick(item: SourceConfigItem.SourceItem)
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
fun onDragHandleTouch(holder: RecyclerView.ViewHolder)
fun onHeaderClick(header: SourceConfigItem.LocaleGroup) fun onHeaderClick(header: SourceConfigItem.LocaleGroup)
} }

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.settings.sources.model package org.koitharu.kotatsu.settings.sources.model
import android.net.Uri import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@ -77,5 +77,32 @@ sealed interface SourceConfigItem {
} }
} }
class Tip(
val key: String,
@DrawableRes val iconResId: Int,
@StringRes val textResId: Int,
) : SourceConfigItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Tip
if (key != other.key) return false
if (iconResId != other.iconResId) return false
if (textResId != other.textResId) return false
return true
}
override fun hashCode(): Int {
var result = key.hashCode()
result = 31 * result + iconResId
result = 31 * result + textResId
return result
}
}
object EmptySearchResult : SourceConfigItem object EmptySearchResult : SourceConfigItem
} }

@ -36,7 +36,7 @@ class FaviconFallbackDrawable(
override fun onBoundsChange(bounds: Rect) { override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds) super.onBoundsChange(bounds)
val innerWidth = bounds.width() - (paint.strokeWidth * 2f) val innerWidth = bounds.width() - (paint.strokeWidth * 2f)
paint.textSize = getTextSizeForWidth(innerWidth, "100%") paint.textSize = getTextSizeForWidth(innerWidth, letter) * 0.5f
paint.getTextBounds(letter, 0, letter.length, textBounds) paint.getTextBounds(letter, 0, letter.length, textBounds)
invalidateSelf() invalidateSelf()
} }

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M4,3L1,6H3V9H1L4,12L7,9H5V6H7L4,3M11,8A1,1 0 0,0 10,9V19L6.8,17.28H6.58C6.3,17.28 6.03,17.39 5.84,17.6L5.1,18.37L10,22.57C10.26,22.85 10.62,23 11,23H17.5A1.5,1.5 0 0,0 19,21.5V17.14C19,16.56 18.68,16.03 18.15,15.79L13.21,13.6L12,13.47V9A1,1 0 0,0 11,8Z" />
</vector>

@ -10,4 +10,4 @@
android:orientation="vertical" android:orientation="vertical"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_source_config" /> tools:listitem="@layout/item_source_config_checkable" />

@ -5,15 +5,17 @@
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="?android:windowBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="?android:listPreferredItemHeightSmall" android:orientation="horizontal"
android:orientation="horizontal"> android:paddingVertical="@dimen/margin_small"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_icon" android:id="@+id/imageView_icon"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_marginHorizontal="?listPreferredItemPaddingStart"
android:background="?colorControlHighlight" android:background="?colorControlHighlight"
android:labelFor="@id/textView_title" android:labelFor="@id/textView_title"
android:scaleType="fitCenter" android:scaleType="fitCenter"
@ -23,7 +25,6 @@
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginStart="?android:listPreferredItemPaddingStart" android:layout_marginStart="?android:listPreferredItemPaddingStart"
android:layout_marginEnd="?android:listPreferredItemPaddingEnd" android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
android:layout_weight="1" android:layout_weight="1"
@ -50,10 +51,34 @@
</LinearLayout> </LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch <ImageView
android:id="@+id/switch_toggle" android:id="@+id/imageView_config"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="?listPreferredItemPaddingEnd" /> android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/settings"
android:padding="@dimen/margin_small"
android:scaleType="center"
android:src="@drawable/ic_settings" />
<ImageView
android:id="@+id/imageView_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/add"
android:padding="@dimen/margin_small"
android:scaleType="center"
android:src="@drawable/ic_add" />
<ImageView
android:id="@+id/imageView_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/remove"
android:padding="@dimen/margin_small"
android:scaleType="center"
android:src="@drawable/ic_delete" />
</LinearLayout> </LinearLayout>

@ -1,26 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
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: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="?android:windowBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="58dp" android:orientation="horizontal"
android:orientation="horizontal"> android:paddingVertical="@dimen/margin_small"
android:paddingStart="?listPreferredItemPaddingStart"
<ImageView android:paddingEnd="?listPreferredItemPaddingEnd">
android:id="@+id/imageView_handle"
android:layout_width="wrap_content" <com.google.android.material.imageview.ShapeableImageView
android:layout_height="match_parent" android:id="@+id/imageView_icon"
android:paddingHorizontal="?listPreferredItemPaddingStart" android:layout_width="32dp"
android:scaleType="center" android:layout_height="32dp"
android:src="@drawable/ic_reorder_handle" /> android:background="?colorControlHighlight"
android:labelFor="@id/textView_title"
android:scaleType="fitCenter"
app:shapeAppearance="?shapeAppearanceCornerSmall"
tools:src="@tools:sample/avatars" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginStart="?android:listPreferredItemPaddingStart" android:layout_marginStart="?android:listPreferredItemPaddingStart"
android:layout_marginEnd="?android:listPreferredItemPaddingEnd" android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
android:layout_weight="1" android:layout_weight="1"
@ -52,14 +55,4 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<ImageView
android:id="@+id/imageView_config"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/settings"
android:paddingHorizontal="?listPreferredItemPaddingEnd"
android:scaleType="center"
android:src="@drawable/ic_settings" />
</LinearLayout> </LinearLayout>

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/margin_normal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="?colorSurfaceVariant"
android:scaleType="center"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle"
tools:src="@drawable/ic_tap_reorder" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/margin_normal"
android:paddingBottom="@dimen/margin_small">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:textAppearance="?textAppearanceBodyMedium"
tools:text="@string/sources_reorder_tip" />
<Button
android:id="@+id/button_close"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:text="@string/got_it" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

@ -15,7 +15,7 @@
<string name="grid">Grid</string> <string name="grid">Grid</string>
<string name="list_mode">List mode</string> <string name="list_mode">List mode</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="remote_sources">Remote sources</string> <string name="remote_sources">Manga sources</string>
<string name="loading_">Loading…</string> <string name="loading_">Loading…</string>
<string name="computing_">Computing…</string> <string name="computing_">Computing…</string>
<string name="chapter_d_of_d">Chapter %1$d of %2$d</string> <string name="chapter_d_of_d">Chapter %1$d of %2$d</string>
@ -424,4 +424,6 @@
<string name="allow_unstable_updates">Allow unstable updates</string> <string name="allow_unstable_updates">Allow unstable updates</string>
<string name="allow_unstable_updates_summary">Propose updates to beta versions of the app</string> <string name="allow_unstable_updates_summary">Propose updates to beta versions of the app</string>
<string name="download_started">Download started</string> <string name="download_started">Download started</string>
<string name="got_it">Got it</string>
<string name="sources_reorder_tip">Tap and hold on an item to reorder them</string>
</resources> </resources>

Loading…
Cancel
Save