Merge branch 'feature/multiselect' into devel
This commit is contained in:
@@ -7,6 +7,7 @@ import android.view.KeyEvent
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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
|
||||||
@@ -20,6 +21,7 @@ import androidx.viewbinding.ViewBinding
|
|||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
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
|
||||||
@@ -36,6 +38,8 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(),
|
|||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
protected val insetsDelegate = WindowInsetsDelegate(this)
|
||||||
|
|
||||||
|
val actionModeDelegate = ActionModeDelegate()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val settings = get<AppSettings>()
|
val settings = get<AppSettings>()
|
||||||
when {
|
when {
|
||||||
@@ -90,8 +94,10 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(),
|
|||||||
return isNight && get<AppSettings>().isAmoledTheme
|
return isNight && get<AppSettings>().isAmoledTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||||
super.onSupportActionModeStarted(mode)
|
super.onSupportActionModeStarted(mode)
|
||||||
|
actionModeDelegate.onSupportActionModeStarted(mode)
|
||||||
val insets = ViewCompat.getRootWindowInsets(binding.root)
|
val insets = ViewCompat.getRootWindowInsets(binding.root)
|
||||||
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
||||||
val view = findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)
|
val view = findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)
|
||||||
@@ -100,6 +106,12 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onSupportActionModeFinished(mode: ActionMode) {
|
||||||
|
super.onSupportActionModeFinished(mode)
|
||||||
|
actionModeDelegate.onSupportActionModeFinished(mode)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if ( // https://issuetracker.google.com/issues/139738913
|
if ( // https://issuetracker.google.com/issues/139738913
|
||||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
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
|
||||||
|
|
||||||
abstract class BaseFragment<B : ViewBinding> : Fragment(),
|
abstract class BaseFragment<B : ViewBinding> :
|
||||||
|
Fragment(),
|
||||||
WindowInsetsDelegate.WindowInsetsListener {
|
WindowInsetsDelegate.WindowInsetsListener {
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
private var viewBinding: B? = null
|
||||||
@@ -23,6 +25,9 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(),
|
|||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
protected val insetsDelegate = WindowInsetsDelegate(this)
|
||||||
|
|
||||||
|
protected val actionModeDelegate: ActionModeDelegate
|
||||||
|
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -47,4 +52,4 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(),
|
|||||||
protected fun bindingOrNull() = viewBinding
|
protected fun bindingOrNull() = viewBinding
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.list.decor
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||||
|
|
||||||
|
abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
private val bounds = Rect()
|
||||||
|
private val boundsF = RectF()
|
||||||
|
private val selection = HashSet<Long>()
|
||||||
|
|
||||||
|
protected var hasBackground: Boolean = true
|
||||||
|
protected var hasForeground: Boolean = false
|
||||||
|
protected var isIncludeDecorAndMargins: Boolean = true
|
||||||
|
|
||||||
|
val checkedItemsCount: Int
|
||||||
|
get() = selection.size
|
||||||
|
|
||||||
|
val checkedItemsIds: Set<Long>
|
||||||
|
get() = selection
|
||||||
|
|
||||||
|
fun toggleItemChecked(id: Long) {
|
||||||
|
if (!selection.remove(id)) {
|
||||||
|
selection.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItemIsChecked(id: Long, isChecked: Boolean) {
|
||||||
|
if (isChecked) {
|
||||||
|
selection.add(id)
|
||||||
|
} else {
|
||||||
|
selection.remove(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkAll(ids: Collection<Long>) {
|
||||||
|
selection.addAll(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSelection() {
|
||||||
|
selection.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
if (hasBackground) {
|
||||||
|
doDraw(canvas, parent, state, false)
|
||||||
|
} else {
|
||||||
|
super.onDraw(canvas, parent, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
if (hasForeground) {
|
||||||
|
doDraw(canvas, parent, state, true)
|
||||||
|
} else {
|
||||||
|
super.onDrawOver(canvas, parent, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State, isOver: Boolean) {
|
||||||
|
val checkpoint = canvas.save()
|
||||||
|
if (parent.clipToPadding) {
|
||||||
|
canvas.clipRect(
|
||||||
|
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
|
||||||
|
parent.height - parent.paddingBottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (child in parent.children) {
|
||||||
|
val itemId = getItemId(parent, child)
|
||||||
|
if (itemId != NO_ID && itemId in selection) {
|
||||||
|
if (isIncludeDecorAndMargins) {
|
||||||
|
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||||
|
} else {
|
||||||
|
bounds.set(child.left, child.top, child.right, child.bottom)
|
||||||
|
}
|
||||||
|
boundsF.set(bounds)
|
||||||
|
boundsF.offset(child.translationX, child.translationY)
|
||||||
|
if (isOver) {
|
||||||
|
onDrawForeground(canvas, parent, child, boundsF, state)
|
||||||
|
} else {
|
||||||
|
onDrawBackground(canvas, parent, child, boundsF, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.restoreToCount(checkpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun getItemId(parent: RecyclerView, child: View) = parent.getChildItemId(child)
|
||||||
|
|
||||||
|
protected open fun onDrawBackground(
|
||||||
|
canvas: Canvas,
|
||||||
|
parent: RecyclerView,
|
||||||
|
child: View,
|
||||||
|
bounds: RectF,
|
||||||
|
state: RecyclerView.State,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
protected open fun onDrawForeground(
|
||||||
|
canvas: Canvas,
|
||||||
|
parent: RecyclerView,
|
||||||
|
child: View,
|
||||||
|
bounds: RectF,
|
||||||
|
state: RecyclerView.State,
|
||||||
|
) = Unit
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.util
|
||||||
|
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
|
||||||
|
class ActionModeDelegate {
|
||||||
|
|
||||||
|
private var activeActionMode: ActionMode? = null
|
||||||
|
private var listeners: MutableList<ActionModeListener>? = null
|
||||||
|
|
||||||
|
val isActionModeStarted: Boolean
|
||||||
|
get() = activeActionMode != null
|
||||||
|
|
||||||
|
fun onSupportActionModeStarted(mode: ActionMode) {
|
||||||
|
activeActionMode = mode
|
||||||
|
listeners?.forEach { it.onActionModeStarted(mode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSupportActionModeFinished(mode: ActionMode) {
|
||||||
|
activeActionMode = null
|
||||||
|
listeners?.forEach { it.onActionModeFinished(mode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addListener(listener: ActionModeListener) {
|
||||||
|
if (listeners == null) {
|
||||||
|
listeners = ArrayList()
|
||||||
|
}
|
||||||
|
checkNotNull(listeners).add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener(listener: ActionModeListener) {
|
||||||
|
listeners?.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addListener(listener: ActionModeListener, owner: LifecycleOwner) {
|
||||||
|
addListener(listener)
|
||||||
|
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ListenerLifecycleObserver(
|
||||||
|
private val listener: ActionModeListener,
|
||||||
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
super.onDestroy(owner)
|
||||||
|
removeListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.util
|
||||||
|
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
|
||||||
|
interface ActionModeListener {
|
||||||
|
|
||||||
|
fun onActionModeStarted(mode: ActionMode)
|
||||||
|
|
||||||
|
fun onActionModeFinished(mode: ActionMode)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
|
||||||
fun Manga.withoutChapters() = if (chapters.isNullOrEmpty()) {
|
fun Manga.withoutChapters() = if (chapters.isNullOrEmpty()) {
|
||||||
this
|
this
|
||||||
@@ -22,4 +23,6 @@ fun Manga.withoutChapters() = if (chapters.isNullOrEmpty()) {
|
|||||||
chapters = null,
|
chapters = null,
|
||||||
source = source,
|
source = source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Collection<Manga>.ids() = mapToSet { it.id }
|
||||||
@@ -24,7 +24,7 @@ class ShortcutsRepository(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val coil: ImageLoader,
|
private val coil: ImageLoader,
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val mangaRepository: MangaDataRepository
|
private val mangaRepository: MangaDataRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val iconSize by lazy {
|
private val iconSize by lazy {
|
||||||
|
|||||||
@@ -178,9 +178,7 @@ class ChaptersFragment :
|
|||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
val manga = viewModel.manga.value
|
|
||||||
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
|
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
|
||||||
mode.title = manga?.title
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,12 +188,7 @@ class ChaptersFragment :
|
|||||||
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
|
||||||
}
|
}
|
||||||
mode.subtitle = resources.getQuantityString(
|
mode.title = items.size.toString()
|
||||||
R.plurals.chapters_from_x,
|
|
||||||
items.size,
|
|
||||||
items.size,
|
|
||||||
chaptersAdapter?.itemCount ?: 0
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -17,6 +16,7 @@ import androidx.appcompat.view.ActionMode
|
|||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
@@ -163,7 +163,7 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(), TabLayoutMediato
|
|||||||
R.id.action_share -> {
|
R.id.action_share -> {
|
||||||
viewModel.manga.value?.let {
|
viewModel.manga.value?.let {
|
||||||
if (it.source == MangaSource.LOCAL) {
|
if (it.source == MangaSource.LOCAL) {
|
||||||
ShareHelper(this).shareCbz(Uri.parse(it.url).toFile())
|
ShareHelper(this).shareCbz(listOf(it.url.toUri().toFile()))
|
||||||
} else {
|
} else {
|
||||||
ShareHelper(this).shareMangaLink(it)
|
ShareHelper(this).shareMangaLink(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,69 +2,32 @@ package org.koitharu.kotatsu.details.ui.adapter
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.Rect
|
import android.graphics.RectF
|
||||||
import androidx.core.content.ContextCompat
|
import android.view.View
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
|
||||||
|
|
||||||
private val bounds = Rect()
|
|
||||||
private val selection = HashSet<Long>()
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
private val radius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
paint.color = ContextCompat.getColor(context, R.color.selector_foreground)
|
paint.color = context.getThemeColor(materialR.attr.colorSecondaryContainer, Color.LTGRAY)
|
||||||
paint.style = Paint.Style.FILL
|
paint.style = Paint.Style.FILL
|
||||||
}
|
}
|
||||||
|
|
||||||
val checkedItemsCount: Int
|
override fun onDrawBackground(
|
||||||
get() = selection.size
|
canvas: Canvas,
|
||||||
|
parent: RecyclerView,
|
||||||
val checkedItemsIds: Set<Long>
|
child: View,
|
||||||
get() = selection
|
bounds: RectF,
|
||||||
|
state: RecyclerView.State,
|
||||||
fun toggleItemChecked(id: Long) {
|
) {
|
||||||
if (!selection.remove(id)) {
|
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||||
selection.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItemIsChecked(id: Long, isChecked: Boolean) {
|
|
||||||
if (isChecked) {
|
|
||||||
selection.add(id)
|
|
||||||
} else {
|
|
||||||
selection.remove(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkAll(ids: Collection<Long>) {
|
|
||||||
selection.addAll(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearSelection() {
|
|
||||||
selection.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
||||||
canvas.save()
|
|
||||||
if (parent.clipToPadding) {
|
|
||||||
canvas.clipRect(
|
|
||||||
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
|
|
||||||
parent.height - parent.paddingBottom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (child in parent.children) {
|
|
||||||
val itemId = parent.getChildItemId(child)
|
|
||||||
if (itemId in selection) {
|
|
||||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
|
||||||
bounds.offset(child.translationX.toInt(), child.translationY.toInt())
|
|
||||||
canvas.drawRect(bounds, paint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
canvas.restore()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,7 @@ import android.os.PowerManager
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import java.util.concurrent.TimeUnit
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlin.collections.set
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
@@ -35,6 +34,7 @@ import org.koitharu.kotatsu.utils.ext.connectivityManager
|
|||||||
import org.koitharu.kotatsu.utils.ext.throttle
|
import org.koitharu.kotatsu.utils.ext.throttle
|
||||||
import org.koitharu.kotatsu.utils.ext.toArraySet
|
import org.koitharu.kotatsu.utils.ext.toArraySet
|
||||||
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class DownloadService : BaseService() {
|
class DownloadService : BaseService() {
|
||||||
|
|
||||||
@@ -187,6 +187,29 @@ class DownloadService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun start(context: Context, manga: Collection<Manga>) {
|
||||||
|
if (manga.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirmDataTransfer(context) {
|
||||||
|
for (item in manga) {
|
||||||
|
val intent = Intent(context, DownloadService::class.java)
|
||||||
|
intent.putExtra(EXTRA_MANGA, ParcelableManga(item))
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirmAndStart(context: Context, items: Set<Manga>) {
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(R.string.save_manga)
|
||||||
|
.setMessage(R.string.batch_manga_save_confirm)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.save) { _, _ ->
|
||||||
|
start(context, items)
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
fun getCancelIntent(startId: Int) = Intent(ACTION_DOWNLOAD_CANCEL)
|
fun getCancelIntent(startId: Int) = Intent(ACTION_DOWNLOAD_CANCEL)
|
||||||
.putExtra(EXTRA_CANCEL_ID, startId)
|
.putExtra(EXTRA_CANCEL_ID, startId)
|
||||||
|
|
||||||
|
|||||||
@@ -43,21 +43,6 @@ class FavouritesRepository(private val db: MangaDatabase) {
|
|||||||
.flatMapLatest { order -> observeAll(categoryId, order) }
|
.flatMapLatest { order -> observeAll(categoryId, order) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getManga(categoryId: Long, offset: Int): List<Manga> {
|
|
||||||
val entities = db.favouritesDao.findAll(categoryId, offset, 20)
|
|
||||||
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getAllCategories(): List<FavouriteCategory> {
|
|
||||||
val entities = db.favouriteCategoriesDao.findAll()
|
|
||||||
return entities.map { it.toFavouriteCategory() }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getCategories(mangaId: Long): List<FavouriteCategory> {
|
|
||||||
val entities = db.favouritesDao.find(mangaId)?.categories
|
|
||||||
return entities?.map { it.toFavouriteCategory() }.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun observeCategories(): Flow<List<FavouriteCategory>> {
|
fun observeCategories(): Flow<List<FavouriteCategory>> {
|
||||||
return db.favouriteCategoriesDao.observeAll().mapItems {
|
return db.favouriteCategoriesDao.observeAll().mapItems {
|
||||||
it.toFavouriteCategory()
|
it.toFavouriteCategory()
|
||||||
@@ -70,8 +55,8 @@ class FavouritesRepository(private val db: MangaDatabase) {
|
|||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeCategoriesIds(mangaId: Long): Flow<List<Long>> {
|
fun observeCategoriesIds(mangaId: Long): Flow<Set<Long>> {
|
||||||
return db.favouritesDao.observeIds(mangaId)
|
return db.favouritesDao.observeIds(mangaId).map { it.toSet() }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addCategory(title: String): FavouriteCategory {
|
suspend fun addCategory(title: String): FavouriteCategory {
|
||||||
@@ -107,22 +92,32 @@ class FavouritesRepository(private val db: MangaDatabase) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addToCategory(manga: Manga, categoryId: Long) {
|
suspend fun addToCategory(categoryId: Long, mangas: Collection<Manga>) {
|
||||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
db.tagsDao.upsert(tags)
|
for (manga in mangas) {
|
||||||
db.mangaDao.upsert(MangaEntity.from(manga), tags)
|
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||||
val entity = FavouriteEntity(manga.id, categoryId, System.currentTimeMillis())
|
db.tagsDao.upsert(tags)
|
||||||
db.favouritesDao.insert(entity)
|
db.mangaDao.upsert(MangaEntity.from(manga), tags)
|
||||||
|
val entity = FavouriteEntity(manga.id, categoryId, System.currentTimeMillis())
|
||||||
|
db.favouritesDao.insert(entity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeFromCategory(manga: Manga, categoryId: Long) {
|
suspend fun removeFromFavourites(ids: Collection<Long>) {
|
||||||
db.favouritesDao.delete(categoryId, manga.id)
|
db.withTransaction {
|
||||||
|
for (id in ids) {
|
||||||
|
db.favouritesDao.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeFromFavourites(manga: Manga) {
|
suspend fun removeFromCategory(categoryId: Long, ids: Collection<Long>) {
|
||||||
db.favouritesDao.delete(manga.id)
|
db.withTransaction {
|
||||||
|
for (id in ids) {
|
||||||
|
db.favouritesDao.delete(categoryId, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeOrder(categoryId: Long): Flow<SortOrder> {
|
private fun observeOrder(categoryId: Long): Flow<SortOrder> {
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ package org.koitharu.kotatsu.favourites.ui
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import java.util.*
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
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.ActionModeListener
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
import org.koitharu.kotatsu.core.ui.titleRes
|
import org.koitharu.kotatsu.core.ui.titleRes
|
||||||
import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding
|
import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding
|
||||||
@@ -21,10 +26,12 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
|||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||||
import org.koitharu.kotatsu.utils.ext.showPopupMenu
|
import org.koitharu.kotatsu.utils.ext.showPopupMenu
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
class FavouritesContainerFragment :
|
||||||
FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback {
|
BaseFragment<FragmentFavouritesBinding>(),
|
||||||
|
FavouritesTabLongClickListener,
|
||||||
|
CategoriesEditDelegate.CategoriesEditCallback,
|
||||||
|
ActionModeListener {
|
||||||
|
|
||||||
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
|
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
|
||||||
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
|
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
@@ -51,6 +58,7 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
|||||||
binding.pager.adapter = adapter
|
binding.pager.adapter = adapter
|
||||||
pagerAdapter = adapter
|
pagerAdapter = adapter
|
||||||
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
|
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
|
||||||
|
actionModeDelegate.addListener(this, viewLifecycleOwner)
|
||||||
|
|
||||||
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged)
|
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||||
@@ -61,6 +69,16 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActionModeStarted(mode: ActionMode) {
|
||||||
|
binding.pager.isUserInputEnabled = false
|
||||||
|
binding.tabs.setTabsEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionModeFinished(mode: ActionMode) {
|
||||||
|
binding.pager.isUserInputEnabled = true
|
||||||
|
binding.tabs.setTabsEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
|
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
|
||||||
binding.root.updatePadding(
|
binding.root.updatePadding(
|
||||||
@@ -146,18 +164,20 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
|||||||
private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) {
|
private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) {
|
||||||
val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return
|
val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return
|
||||||
for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) {
|
for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) {
|
||||||
val menuItem = submenu.add(
|
val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes)
|
||||||
R.id.group_order,
|
|
||||||
Menu.NONE,
|
|
||||||
i,
|
|
||||||
item.titleRes
|
|
||||||
)
|
|
||||||
menuItem.isCheckable = true
|
menuItem.isCheckable = true
|
||||||
menuItem.isChecked = item == category.order
|
menuItem.isChecked = item == category.order
|
||||||
}
|
}
|
||||||
submenu.setGroupCheckable(R.id.group_order, true, true)
|
submenu.setGroupCheckable(R.id.group_order, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun TabLayout.setTabsEnabled(enabled: Boolean) {
|
||||||
|
val tabStrip = getChildAt(0) as? ViewGroup ?: return
|
||||||
|
for (tab in tabStrip.children) {
|
||||||
|
tab.isEnabled = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance() = FavouritesContainerFragment()
|
fun newInstance() = FavouritesContainerFragment()
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ package org.koitharu.kotatsu.favourites.ui.categories.select
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
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.domain.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
|
import org.koitharu.kotatsu.core.model.withoutChapters
|
||||||
import org.koitharu.kotatsu.databinding.DialogFavoriteCategoriesBinding
|
import org.koitharu.kotatsu.databinding.DialogFavoriteCategoriesBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
|
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
|
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
|
||||||
@@ -26,10 +28,10 @@ class FavouriteCategoriesDialog :
|
|||||||
BaseBottomSheet<DialogFavoriteCategoriesBinding>(),
|
BaseBottomSheet<DialogFavoriteCategoriesBinding>(),
|
||||||
OnListItemClickListener<MangaCategoryItem>,
|
OnListItemClickListener<MangaCategoryItem>,
|
||||||
CategoriesEditDelegate.CategoriesEditCallback,
|
CategoriesEditDelegate.CategoriesEditCallback,
|
||||||
View.OnClickListener {
|
Toolbar.OnMenuItemClickListener {
|
||||||
|
|
||||||
private val viewModel by viewModel<MangaCategoriesViewModel> {
|
private val viewModel by viewModel<MangaCategoriesViewModel> {
|
||||||
parametersOf(requireNotNull(arguments?.getParcelable<ParcelableManga>(MangaIntent.KEY_MANGA)).manga)
|
parametersOf(requireNotNull(arguments?.getParcelableArrayList<ParcelableManga>(KEY_MANGA_LIST)).map { it.manga })
|
||||||
}
|
}
|
||||||
|
|
||||||
private var adapter: MangaCategoriesAdapter? = null
|
private var adapter: MangaCategoriesAdapter? = null
|
||||||
@@ -46,7 +48,7 @@ class FavouriteCategoriesDialog :
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
adapter = MangaCategoriesAdapter(this)
|
adapter = MangaCategoriesAdapter(this)
|
||||||
binding.recyclerViewCategories.adapter = adapter
|
binding.recyclerViewCategories.adapter = adapter
|
||||||
binding.textViewAdd.setOnClickListener(this)
|
binding.toolbar.setOnMenuItemClickListener(this)
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||||
@@ -57,9 +59,13 @@ class FavouriteCategoriesDialog :
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
when (v.id) {
|
return when (item.itemId) {
|
||||||
R.id.textView_add -> editDelegate.createCategory()
|
R.id.action_create -> {
|
||||||
|
editDelegate.createCategory()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,10 +92,15 @@ class FavouriteCategoriesDialog :
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "FavouriteCategoriesDialog"
|
private const val TAG = "FavouriteCategoriesDialog"
|
||||||
|
private const val KEY_MANGA_LIST = "manga_list"
|
||||||
|
|
||||||
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog()
|
fun show(fm: FragmentManager, manga: Manga) = Companion.show(fm, listOf(manga))
|
||||||
.withArgs(1) {
|
|
||||||
putParcelable(MangaIntent.KEY_MANGA, ParcelableManga(manga))
|
fun show(fm: FragmentManager, manga: Collection<Manga>) = FavouriteCategoriesDialog().withArgs(1) {
|
||||||
}.show(fm, TAG)
|
putParcelableArrayList(
|
||||||
|
KEY_MANGA_LIST,
|
||||||
|
manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it.withoutChapters()) }
|
||||||
|
)
|
||||||
|
}.show(fm, TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,19 +4,20 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.model.ids
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
|
|
||||||
class MangaCategoriesViewModel(
|
class MangaCategoriesViewModel(
|
||||||
private val manga: Manga,
|
private val manga: List<Manga>,
|
||||||
private val favouritesRepository: FavouritesRepository
|
private val favouritesRepository: FavouritesRepository
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val content = combine(
|
val content = combine(
|
||||||
favouritesRepository.observeCategories(),
|
favouritesRepository.observeCategories(),
|
||||||
favouritesRepository.observeCategoriesIds(manga.id)
|
observeCategoriesIds(),
|
||||||
) { all, checked ->
|
) { all, checked ->
|
||||||
all.map {
|
all.map {
|
||||||
MangaCategoryItem(
|
MangaCategoryItem(
|
||||||
@@ -30,9 +31,9 @@ class MangaCategoriesViewModel(
|
|||||||
fun setChecked(categoryId: Long, isChecked: Boolean) {
|
fun setChecked(categoryId: Long, isChecked: Boolean) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
favouritesRepository.addToCategory(manga, categoryId)
|
favouritesRepository.addToCategory(categoryId, manga)
|
||||||
} else {
|
} else {
|
||||||
favouritesRepository.removeFromCategory(manga, categoryId)
|
favouritesRepository.removeFromCategory(categoryId, manga.ids())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,4 +43,25 @@ class MangaCategoriesViewModel(
|
|||||||
favouritesRepository.addCategory(name)
|
favouritesRepository.addCategory(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeCategoriesIds() = if (manga.size == 1) {
|
||||||
|
// Fast path
|
||||||
|
favouritesRepository.observeCategoriesIds(manga[0].id)
|
||||||
|
} else {
|
||||||
|
combine(
|
||||||
|
manga.map { favouritesRepository.observeCategoriesIds(it.id) }
|
||||||
|
) { array ->
|
||||||
|
val result = HashSet<Long>()
|
||||||
|
var isFirst = true
|
||||||
|
for (ids in array) {
|
||||||
|
if (isFirst) {
|
||||||
|
result.addAll(ids)
|
||||||
|
isFirst = false
|
||||||
|
} else {
|
||||||
|
result.retainAll(ids.toSet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.list
|
package org.koitharu.kotatsu.favourites.ui.list
|
||||||
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
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.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
class FavouritesListFragment : MangaListFragment() {
|
class FavouritesListFragment : MangaListFragment() {
|
||||||
@@ -23,17 +22,20 @@ class FavouritesListFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = Unit
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
super.onCreatePopupMenu(inflater, menu, data)
|
mode.menuInflater.inflate(R.menu.mode_favourites, menu)
|
||||||
inflater.inflate(R.menu.popup_favourites, menu)
|
return super.onCreateActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga) = when (item.itemId) {
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
R.id.action_remove -> {
|
return when (item.itemId) {
|
||||||
viewModel.removeFromFavourites(data)
|
R.id.action_remove -> {
|
||||||
true
|
viewModel.removeFromFavourites(selectedItemsIds)
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onActionItemClicked(mode, item)
|
||||||
}
|
}
|
||||||
else -> super.onPopupMenuItemSelected(item, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState
|
|||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
@@ -56,12 +55,15 @@ class FavouritesListViewModel(
|
|||||||
|
|
||||||
override fun onRetry() = Unit
|
override fun onRetry() = Unit
|
||||||
|
|
||||||
fun removeFromFavourites(manga: Manga) {
|
fun removeFromFavourites(ids: Set<Long>) {
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
launchJob {
|
launchJob {
|
||||||
if (categoryId == 0L) {
|
if (categoryId == 0L) {
|
||||||
repository.removeFromFavourites(manga)
|
repository.removeFromFavourites(ids)
|
||||||
} else {
|
} else {
|
||||||
repository.removeFromCategory(manga, categoryId)
|
repository.removeFromCategory(categoryId, ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ class HistoryRepository(
|
|||||||
db.historyDao.delete(manga.id)
|
db.historyDao.delete(manga.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun delete(ids: Collection<Long>) {
|
||||||
|
db.withTransaction {
|
||||||
|
for (id in ids) {
|
||||||
|
db.historyDao.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to replace one manga with another one
|
* Try to replace one manga with another one
|
||||||
* Useful for replacing saved manga on deleting it with remove source
|
* Useful for replacing saved manga on deleting it with remove source
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import android.view.Menu
|
|||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.util.ellipsize
|
|
||||||
|
|
||||||
class HistoryListFragment : MangaListFragment() {
|
class HistoryListFragment : MangaListFragment() {
|
||||||
|
|
||||||
@@ -20,7 +18,6 @@ class HistoryListFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
|
||||||
viewModel.isGroupingEnabled.observe(viewLifecycleOwner) {
|
viewModel.isGroupingEnabled.observe(viewLifecycleOwner) {
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
@@ -59,30 +56,22 @@ class HistoryListFragment : MangaListFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
super.onCreatePopupMenu(inflater, menu, data)
|
mode.menuInflater.inflate(R.menu.mode_history, menu)
|
||||||
inflater.inflate(R.menu.popup_history, menu)
|
return super.onCreateActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean {
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_remove -> {
|
R.id.action_remove -> {
|
||||||
viewModel.removeFromHistory(data)
|
viewModel.removeFromHistory(selectedItemsIds)
|
||||||
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onPopupMenuItemSelected(item, data)
|
else -> super.onActionItemClicked(mode, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemRemoved(item: Manga) {
|
|
||||||
Snackbar.make(
|
|
||||||
binding.recyclerView, getString(
|
|
||||||
R.string._s_removed_from_history,
|
|
||||||
item.title.ellipsize(16)
|
|
||||||
), Snackbar.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance() = HistoryListFragment()
|
fun newInstance() = HistoryListFragment()
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.history.ui
|
|||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -13,14 +15,10 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|||||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.*
|
import org.koitharu.kotatsu.list.ui.model.*
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class HistoryListViewModel(
|
class HistoryListViewModel(
|
||||||
private val repository: HistoryRepository,
|
private val repository: HistoryRepository,
|
||||||
@@ -29,7 +27,6 @@ class HistoryListViewModel(
|
|||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
) : MangaListViewModel(settings) {
|
) : MangaListViewModel(settings) {
|
||||||
|
|
||||||
val onItemRemoved = SingleLiveEvent<Manga>()
|
|
||||||
val isGroupingEnabled = MutableLiveData<Boolean>()
|
val isGroupingEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
private val historyGrouping = settings.observe()
|
private val historyGrouping = settings.observe()
|
||||||
@@ -72,10 +69,12 @@ class HistoryListViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromHistory(manga: Manga) {
|
fun removeFromHistory(ids: Set<Long>) {
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
launchJob {
|
launchJob {
|
||||||
repository.delete(manga)
|
repository.delete(ids)
|
||||||
onItemRemoved.call(manga)
|
|
||||||
shortcutsRepository.updateShortcuts()
|
shortcutsRepository.updateShortcuts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ 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.widget.PopupMenu
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.collection.ArraySet
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.isNotEmpty
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
@@ -24,24 +26,31 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
|||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||||
import org.koitharu.kotatsu.main.ui.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.AppBarOwner
|
||||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
|
|
||||||
abstract class MangaListFragment :
|
abstract class MangaListFragment :
|
||||||
BaseFragment<FragmentListBinding>(),
|
BaseFragment<FragmentListBinding>(),
|
||||||
PaginationScrollListener.Callback,
|
PaginationScrollListener.Callback,
|
||||||
MangaListListener,
|
MangaListListener,
|
||||||
SwipeRefreshLayout.OnRefreshListener {
|
SwipeRefreshLayout.OnRefreshListener,
|
||||||
|
ActionMode.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 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 {
|
||||||
@@ -51,6 +60,12 @@ abstract class MangaListFragment :
|
|||||||
|
|
||||||
protected abstract val viewModel: MangaListViewModel
|
protected abstract val viewModel: MangaListViewModel
|
||||||
|
|
||||||
|
protected val selectedItemsIds: Set<Long>
|
||||||
|
get() = selectionDecoration?.checkedItemsIds?.toSet().orEmpty()
|
||||||
|
|
||||||
|
protected val selectedItems: Set<Manga>
|
||||||
|
get() = collectSelectedItems()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
@@ -68,10 +83,12 @@ abstract class MangaListFragment :
|
|||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
listener = this,
|
listener = this,
|
||||||
)
|
)
|
||||||
|
selectionDecoration = MangaSelectionDecoration(view.context)
|
||||||
paginationListener = PaginationScrollListener(4, this)
|
paginationListener = PaginationScrollListener(4, this)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
adapter = listAdapter
|
adapter = listAdapter
|
||||||
|
addItemDecoration(selectionDecoration!!)
|
||||||
addOnScrollListener(paginationListener!!)
|
addOnScrollListener(paginationListener!!)
|
||||||
}
|
}
|
||||||
with(binding.swipeRefreshLayout) {
|
with(binding.swipeRefreshLayout) {
|
||||||
@@ -91,6 +108,7 @@ abstract class MangaListFragment :
|
|||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
listAdapter = null
|
listAdapter = null
|
||||||
paginationListener = null
|
paginationListener = null
|
||||||
|
selectionDecoration = null
|
||||||
spanSizeLookup.invalidateCache()
|
spanSizeLookup.invalidateCache()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
@@ -109,22 +127,28 @@ abstract class MangaListFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Manga, view: View) {
|
override fun onItemClick(item: Manga, view: View) {
|
||||||
|
if (selectionDecoration?.checkedItemsCount != 0) {
|
||||||
|
selectionDecoration?.toggleItemChecked(item.id)
|
||||||
|
if (selectionDecoration?.checkedItemsCount == 0) {
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
actionMode?.invalidate()
|
||||||
|
binding.recyclerView.invalidateItemDecorations()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: Manga, view: View): Boolean {
|
override fun onItemLongClick(item: Manga, view: View): Boolean {
|
||||||
val menu = PopupMenu(context ?: return false, view)
|
if (actionMode == null) {
|
||||||
onCreatePopupMenu(menu.menuInflater, menu.menu, item)
|
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||||
return if (menu.menu.hasVisibleItems()) {
|
|
||||||
menu.setOnMenuItemClickListener {
|
|
||||||
onPopupMenuItemSelected(it, item)
|
|
||||||
}
|
|
||||||
menu.gravity = GravityCompat.END or Gravity.TOP
|
|
||||||
menu.show()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
return actionMode?.also {
|
||||||
|
selectionDecoration?.setItemIsChecked(item.id, true)
|
||||||
|
binding.recyclerView.invalidateItemDecorations()
|
||||||
|
it.invalidate()
|
||||||
|
} != null
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
@@ -238,12 +262,67 @@ abstract class MangaListFragment :
|
|||||||
addOnLayoutChangeListener(spanResolver)
|
addOnLayoutChangeListener(spanResolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
selectionDecoration?.let { addItemDecoration(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) = Unit
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
return menu.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun onPopupMenuItemSelected(item: MenuItem, data: Manga) = false
|
@CallSuper
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.title = selectionDecoration?.checkedItemsCount?.toString()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.action_select_all -> {
|
||||||
|
val ids = listAdapter?.items?.mapNotNull {
|
||||||
|
(it as? MangaItemModel)?.id
|
||||||
|
} ?: return false
|
||||||
|
selectionDecoration?.checkAll(ids)
|
||||||
|
binding.recyclerView.invalidateItemDecorations()
|
||||||
|
mode.invalidate()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_share -> {
|
||||||
|
ShareHelper(requireContext()).shareMangaLinks(selectedItems)
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_favourite -> {
|
||||||
|
FavouriteCategoriesDialog.show(childFragmentManager, selectedItems)
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_save -> {
|
||||||
|
DownloadService.confirmAndStart(requireContext(), selectedItems)
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
selectionDecoration?.clearSelection()
|
||||||
|
binding.recyclerView.invalidateItemDecorations()
|
||||||
|
actionMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectSelectedItems(): Set<Manga> {
|
||||||
|
val checkedIds = selectionDecoration?.checkedItemsIds ?: return emptySet()
|
||||||
|
val items = listAdapter?.items ?: return emptySet()
|
||||||
|
val result = ArraySet<Manga>(checkedIds.size)
|
||||||
|
for (item in items) {
|
||||||
|
if (item is MangaItemModel && item.id in checkedIds) {
|
||||||
|
result.add(item.manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.view.View
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
|
||||||
|
|
||||||
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||||
|
private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||||
|
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||||
|
private val fillColor = ColorUtils.setAlphaComponent(
|
||||||
|
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||||
|
0x74
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
hasBackground = false
|
||||||
|
hasForeground = true
|
||||||
|
isIncludeDecorAndMargins = false
|
||||||
|
|
||||||
|
paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width)
|
||||||
|
checkIcon?.setTint(strokeColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(parent: RecyclerView, child: View): Long {
|
||||||
|
val holder = parent.getChildViewHolder(child) ?: return NO_ID
|
||||||
|
val item = holder.getItem<MangaItemModel>() ?: return NO_ID
|
||||||
|
return item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawForeground(
|
||||||
|
canvas: Canvas,
|
||||||
|
parent: RecyclerView,
|
||||||
|
child: View,
|
||||||
|
bounds: RectF,
|
||||||
|
state: RecyclerView.State,
|
||||||
|
) {
|
||||||
|
val radius = (child as? CardView)?.radius ?: 0f
|
||||||
|
paint.color = fillColor
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||||
|
paint.color = strokeColor
|
||||||
|
paint.style = Paint.Style.STROKE
|
||||||
|
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||||
|
checkIcon?.run {
|
||||||
|
setBounds(
|
||||||
|
(bounds.left + iconOffset).toInt(),
|
||||||
|
(bounds.top + iconOffset).toInt(),
|
||||||
|
(bounds.left + iconOffset + intrinsicWidth).toInt(),
|
||||||
|
(bounds.top + iconOffset + intrinsicHeight).toInt(),
|
||||||
|
)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ package org.koitharu.kotatsu.list.ui.model
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
data class MangaGridModel(
|
data class MangaGridModel(
|
||||||
val id: Long,
|
override val id: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val coverUrl: String,
|
val coverUrl: String,
|
||||||
val manga: Manga,
|
override val manga: Manga,
|
||||||
val counter: Int,
|
val counter: Int,
|
||||||
) : ListModel
|
) : MangaItemModel
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
|
sealed interface MangaItemModel : ListModel {
|
||||||
|
|
||||||
|
val id: Long
|
||||||
|
val manga: Manga
|
||||||
|
}
|
||||||
@@ -3,12 +3,12 @@ package org.koitharu.kotatsu.list.ui.model
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
data class MangaListDetailedModel(
|
data class MangaListDetailedModel(
|
||||||
val id: Long,
|
override val id: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val subtitle: String?,
|
val subtitle: String?,
|
||||||
val tags: String,
|
val tags: String,
|
||||||
val coverUrl: String,
|
val coverUrl: String,
|
||||||
val rating: String?,
|
val rating: String?,
|
||||||
val manga: Manga,
|
override val manga: Manga,
|
||||||
val counter: Int,
|
val counter: Int,
|
||||||
) : ListModel
|
) : MangaItemModel
|
||||||
@@ -3,10 +3,10 @@ package org.koitharu.kotatsu.list.ui.model
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
data class MangaListModel(
|
data class MangaListModel(
|
||||||
val id: Long,
|
override val id: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val subtitle: String,
|
val subtitle: String,
|
||||||
val coverUrl: String,
|
val coverUrl: String,
|
||||||
val manga: Manga,
|
override val manga: Manga,
|
||||||
val counter: Int,
|
val counter: Int,
|
||||||
) : ListModel
|
) : MangaItemModel
|
||||||
@@ -9,6 +9,9 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
@@ -16,8 +19,7 @@ import org.koitharu.kotatsu.BuildConfig
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
import org.koitharu.kotatsu.parsers.util.ellipsize
|
|
||||||
import org.koitharu.kotatsu.utils.progress.Progress
|
import org.koitharu.kotatsu.utils.progress.Progress
|
||||||
|
|
||||||
class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmSuppressWildcards Uri>> {
|
class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmSuppressWildcards Uri>> {
|
||||||
@@ -46,7 +48,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
viewModel.onMangaRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
viewModel.onMangaRemoved.observe(viewLifecycleOwner) { onItemRemoved() }
|
||||||
viewModel.importProgress.observe(viewLifecycleOwner, ::onImportProgressChanged)
|
viewModel.importProgress.observe(viewLifecycleOwner, ::onImportProgressChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,35 +99,41 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
|
|||||||
viewModel.importFiles(result)
|
viewModel.importFiles(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
super.onCreatePopupMenu(inflater, menu, data)
|
mode.menuInflater.inflate(R.menu.mode_local, menu)
|
||||||
inflater.inflate(R.menu.popup_local, menu)
|
return super.onCreateActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean {
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_delete -> {
|
R.id.action_remove -> {
|
||||||
MaterialAlertDialogBuilder(context ?: return false)
|
showDeletionConfirm(selectedItemsIds, mode)
|
||||||
.setTitle(R.string.delete_manga)
|
|
||||||
.setMessage(getString(R.string.text_delete_local_manga, data.title))
|
|
||||||
.setPositiveButton(R.string.delete) { _, _ ->
|
|
||||||
viewModel.delete(data)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onPopupMenuItemSelected(item, data)
|
R.id.action_share -> {
|
||||||
|
val files = selectedItems.map { it.url.toUri().toFile() }
|
||||||
|
ShareHelper(requireContext()).shareCbz(files)
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onActionItemClicked(mode, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemRemoved(item: Manga) {
|
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
|
||||||
Snackbar.make(
|
MaterialAlertDialogBuilder(context ?: return)
|
||||||
binding.recyclerView, getString(
|
.setTitle(R.string.delete_manga)
|
||||||
R.string._s_deleted_from_local_storage,
|
.setMessage(getString(R.string.text_delete_local_manga_batch))
|
||||||
item.title.ellipsize(16)
|
.setPositiveButton(R.string.delete) { _, _ ->
|
||||||
), Snackbar.LENGTH_SHORT
|
viewModel.delete(ids)
|
||||||
).show()
|
mode.finish()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onItemRemoved() {
|
||||||
|
Snackbar.make(binding.recyclerView, R.string.removal_completed, Snackbar.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onImportProgressChanged(progress: Progress?) {
|
private fun onImportProgressChanged(progress: Progress?) {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package org.koitharu.kotatsu.local.ui
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import java.io.IOException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||||
@@ -19,7 +21,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.progress.Progress
|
import org.koitharu.kotatsu.utils.progress.Progress
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class LocalListViewModel(
|
class LocalListViewModel(
|
||||||
private val repository: LocalMangaRepository,
|
private val repository: LocalMangaRepository,
|
||||||
@@ -28,7 +29,7 @@ class LocalListViewModel(
|
|||||||
private val shortcutsRepository: ShortcutsRepository,
|
private val shortcutsRepository: ShortcutsRepository,
|
||||||
) : MangaListViewModel(settings) {
|
) : MangaListViewModel(settings) {
|
||||||
|
|
||||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
val onMangaRemoved = SingleLiveEvent<Unit>()
|
||||||
val importProgress = MutableLiveData<Progress?>(null)
|
val importProgress = MutableLiveData<Progress?>(null)
|
||||||
private val listError = MutableStateFlow<Throwable?>(null)
|
private val listError = MutableStateFlow<Throwable?>(null)
|
||||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||||
@@ -87,18 +88,23 @@ class LocalListViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(manga: Manga) {
|
fun delete(ids: Set<Long>) {
|
||||||
launchJob {
|
launchLoadingJob {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val original = repository.getRemoteManga(manga)
|
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
|
||||||
repository.delete(manga) || throw IOException("Unable to delete file")
|
for (manga in itemsToRemove) {
|
||||||
runCatching {
|
val original = repository.getRemoteManga(manga)
|
||||||
historyRepository.deleteOrSwap(manga, original)
|
repository.delete(manga) || throw IOException("Unable to delete file")
|
||||||
|
runCatching {
|
||||||
|
historyRepository.deleteOrSwap(manga, original)
|
||||||
|
}
|
||||||
|
mangaList.update { list ->
|
||||||
|
list?.filterNot { it.id == manga.id }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mangaList.value = mangaList.value?.filterNot { it.id == manga.id }
|
|
||||||
}
|
}
|
||||||
shortcutsRepository.updateShortcuts()
|
shortcutsRepository.updateShortcuts()
|
||||||
onMangaRemoved.call(manga)
|
onMangaRemoved.call(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
@@ -288,6 +289,16 @@ class MainActivity :
|
|||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||||
|
super.onSupportActionModeStarted(mode)
|
||||||
|
adjustDrawerLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportActionModeFinished(mode: ActionMode) {
|
||||||
|
super.onSupportActionModeFinished(mode)
|
||||||
|
adjustDrawerLock()
|
||||||
|
}
|
||||||
|
|
||||||
private fun onOpenReader(manga: Manga) {
|
private fun onOpenReader(manga: Manga) {
|
||||||
val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
ActivityOptions.makeClipRevealAnimation(
|
ActivityOptions.makeClipRevealAnimation(
|
||||||
@@ -361,14 +372,14 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onSearchOpened() {
|
private fun onSearchOpened() {
|
||||||
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
|
||||||
drawerToggle?.isDrawerIndicatorEnabled = false
|
drawerToggle?.isDrawerIndicatorEnabled = false
|
||||||
|
adjustDrawerLock()
|
||||||
adjustFabVisibility(isSearchOpened = true)
|
adjustFabVisibility(isSearchOpened = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSearchClosed() {
|
private fun onSearchClosed() {
|
||||||
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
|
|
||||||
drawerToggle?.isDrawerIndicatorEnabled = true
|
drawerToggle?.isDrawerIndicatorEnabled = true
|
||||||
|
adjustDrawerLock()
|
||||||
adjustFabVisibility(isSearchOpened = false)
|
adjustFabVisibility(isSearchOpened = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,4 +402,12 @@ class MainActivity :
|
|||||||
) {
|
) {
|
||||||
if (!isSearchOpened && topFragment is HistoryListFragment) binding.fab.show() else binding.fab.hide()
|
if (!isSearchOpened && topFragment is HistoryListFragment) binding.fab.show() else binding.fab.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun adjustDrawerLock() {
|
||||||
|
val drawer = drawer ?: return
|
||||||
|
val isLocked = actionModeDelegate.isActionModeStarted || (drawerToggle?.isDrawerIndicatorEnabled == false)
|
||||||
|
drawer.setDrawerLockMode(
|
||||||
|
if (isLocked) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.remotelist.ui
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
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
|
||||||
@@ -46,6 +47,11 @@ class RemoteListFragment : MangaListFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
||||||
|
return super.onCreateActionMode(mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFilterClick() {
|
override fun onFilterClick() {
|
||||||
FilterBottomSheet.show(childFragmentManager)
|
FilterBottomSheet.show(childFragmentManager)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.search.ui
|
package org.koitharu.kotatsu.search.ui
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
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.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.ext.serializableArgument
|
import org.koitharu.kotatsu.utils.ext.serializableArgument
|
||||||
@@ -21,6 +24,11 @@ class SearchFragment : MangaListFragment() {
|
|||||||
viewModel.loadNextPage()
|
viewModel.loadNextPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
||||||
|
return super.onCreateActionMode(mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ARG_QUERY = "query"
|
private const val ARG_QUERY = "query"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.search.ui.global
|
package org.koitharu.kotatsu.search.ui.global
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
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.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
import org.koitharu.kotatsu.utils.ext.stringArgument
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
|
|
||||||
class GlobalSearchFragment : MangaListFragment() {
|
class GlobalSearchFragment : MangaListFragment() {
|
||||||
|
|
||||||
override val viewModel by viewModel<GlobalSearchViewModel> {
|
override val viewModel by viewModel<GlobalSearchViewModel> {
|
||||||
@@ -17,6 +19,11 @@ class GlobalSearchFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = Unit
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
||||||
|
return super.onCreateActionMode(mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ARG_QUERY = "query"
|
private const val ARG_QUERY = "query"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Bundle
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -46,6 +47,11 @@ class SuggestionsFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = Unit
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
||||||
|
return super.onCreateActionMode(mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance() = SuggestionsFragment()
|
fun newInstance() = SuggestionsFragment()
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
|||||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||||
import org.koitharu.kotatsu.utils.progress.Progress
|
import org.koitharu.kotatsu.utils.progress.Progress
|
||||||
|
|
||||||
class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListener.Callback,
|
class FeedFragment :
|
||||||
|
BaseFragment<FragmentFeedBinding>(),
|
||||||
|
PaginationScrollListener.Callback,
|
||||||
MangaListListener {
|
MangaListListener {
|
||||||
|
|
||||||
private val viewModel by viewModel<FeedViewModel>()
|
private val viewModel by viewModel<FeedViewModel>()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui
|
package org.koitharu.kotatsu.tracker.ui
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -12,13 +11,13 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
|
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
|
||||||
|
|
||||||
class FeedViewModel(
|
class FeedViewModel(
|
||||||
private val repository: TrackingRepository
|
private val repository: TrackingRepository
|
||||||
@@ -27,30 +26,34 @@ class FeedViewModel(
|
|||||||
private val logList = MutableStateFlow<List<TrackingLogItem>?>(null)
|
private val logList = MutableStateFlow<List<TrackingLogItem>?>(null)
|
||||||
private val hasNextPage = MutableStateFlow(false)
|
private val hasNextPage = MutableStateFlow(false)
|
||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
|
private val header = ListHeader(null, R.string.updates, null)
|
||||||
|
|
||||||
val isEmptyState = MutableLiveData(false)
|
|
||||||
val onFeedCleared = SingleLiveEvent<Unit>()
|
val onFeedCleared = SingleLiveEvent<Unit>()
|
||||||
val content = combine(
|
val content = combine(
|
||||||
logList.filterNotNull().mapItems {
|
logList.filterNotNull(),
|
||||||
it.toFeedItem()
|
|
||||||
},
|
|
||||||
hasNextPage
|
hasNextPage
|
||||||
) { list, isHasNextPage ->
|
) { list, isHasNextPage ->
|
||||||
when {
|
buildList(list.size + 2) {
|
||||||
list.isEmpty() -> listOf(
|
add(header)
|
||||||
EmptyState(
|
if (list.isEmpty()) {
|
||||||
icon = R.drawable.ic_feed,
|
add(
|
||||||
textPrimary = R.string.text_empty_holder_primary,
|
EmptyState(
|
||||||
textSecondary = R.string.text_feed_holder,
|
icon = R.drawable.ic_feed,
|
||||||
actionStringRes = 0,
|
textPrimary = R.string.text_empty_holder_primary,
|
||||||
|
textSecondary = R.string.text_feed_holder,
|
||||||
|
actionStringRes = 0,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
isHasNextPage -> list + LoadingFooter
|
list.mapTo(this) { it.toFeedItem() }
|
||||||
else -> list
|
if (isHasNextPage) {
|
||||||
|
add(LoadingFooter)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.asLiveDataDistinct(
|
}.asLiveDataDistinct(
|
||||||
viewModelScope.coroutineContext + Dispatchers.Default,
|
viewModelScope.coroutineContext + Dispatchers.Default,
|
||||||
listOf(LoadingState)
|
listOf(header, LoadingState)
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -66,7 +69,6 @@ class FeedViewModel(
|
|||||||
val list = repository.getTrackingLog(offset, 20)
|
val list = repository.getTrackingLog(offset, 20)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
logList.value = list
|
logList.value = list
|
||||||
isEmptyState.postValue(list.isEmpty())
|
|
||||||
} else if (list.isNotEmpty()) {
|
} else if (list.isNotEmpty()) {
|
||||||
logList.value = logList.value?.plus(list) ?: list
|
logList.value = logList.value?.plus(list) ?: list
|
||||||
}
|
}
|
||||||
@@ -80,7 +82,6 @@ class FeedViewModel(
|
|||||||
lastJob?.cancelAndJoin()
|
lastJob?.cancelAndJoin()
|
||||||
repository.clearLogs()
|
repository.clearLogs()
|
||||||
logList.value = emptyList()
|
logList.value = emptyList()
|
||||||
isEmptyState.postValue(true)
|
|
||||||
onFeedCleared.postCall(Unit)
|
onFeedCleared.postCall(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import kotlin.jvm.internal.Intrinsics
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.*
|
import org.koitharu.kotatsu.list.ui.adapter.*
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
|
||||||
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
|
|
||||||
class FeedAdapter(
|
class FeedAdapter(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -24,6 +23,7 @@ class FeedAdapter(
|
|||||||
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
|
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
|
||||||
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
|
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
|
||||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
|
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
|
||||||
|
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||||
@@ -32,10 +32,7 @@ class FeedAdapter(
|
|||||||
oldItem is FeedItem && newItem is FeedItem -> {
|
oldItem is FeedItem && newItem is FeedItem -> {
|
||||||
oldItem.id == newItem.id
|
oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
oldItem == LoadingFooter && newItem == LoadingFooter -> {
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
@@ -51,5 +48,6 @@ class FeedAdapter(
|
|||||||
const val ITEM_TYPE_ERROR_STATE = 3
|
const val ITEM_TYPE_ERROR_STATE = 3
|
||||||
const val ITEM_TYPE_ERROR_FOOTER = 4
|
const val ITEM_TYPE_ERROR_FOOTER = 4
|
||||||
const val ITEM_TYPE_EMPTY = 5
|
const val ITEM_TYPE_EMPTY = 5
|
||||||
|
const val ITEM_TYPE_HEADER = 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,62 +1,82 @@
|
|||||||
package org.koitharu.kotatsu.utils
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.io.File
|
|
||||||
|
private const val TYPE_TEXT = "text/plain"
|
||||||
|
private const val TYPE_IMAGE = "image/*"
|
||||||
|
private const val TYPE_CBZ = "application/x-cbz"
|
||||||
|
|
||||||
class ShareHelper(private val context: Context) {
|
class ShareHelper(private val context: Context) {
|
||||||
|
|
||||||
fun shareMangaLink(manga: Manga) {
|
fun shareMangaLink(manga: Manga) {
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
val text = buildString {
|
||||||
intent.type = "text/plain"
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, buildString {
|
|
||||||
append(manga.title)
|
append(manga.title)
|
||||||
append("\n \n")
|
append("\n \n")
|
||||||
append(manga.publicUrl)
|
append(manga.publicUrl)
|
||||||
})
|
}
|
||||||
val shareIntent =
|
ShareCompat.IntentBuilder(context)
|
||||||
Intent.createChooser(intent, context.getString(R.string.share_s, manga.title))
|
.setText(text)
|
||||||
context.startActivity(shareIntent)
|
.setType(TYPE_TEXT)
|
||||||
|
.setChooserTitle(context.getString(R.string.share_s, manga.title))
|
||||||
|
.startChooser()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareCbz(file: File) {
|
fun shareMangaLinks(manga: Collection<Manga>) {
|
||||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", file)
|
if (manga.isEmpty()) {
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
return
|
||||||
intent.setDataAndType(uri, context.contentResolver.getType(uri))
|
}
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
if (manga.size == 1) {
|
||||||
val shareIntent =
|
shareMangaLink(manga.first())
|
||||||
Intent.createChooser(intent, context.getString(R.string.share_s, file.name))
|
return
|
||||||
context.startActivity(shareIntent)
|
}
|
||||||
|
val text = manga.joinToString("\n \n") {
|
||||||
|
"${it.title} - ${it.publicUrl}"
|
||||||
|
}
|
||||||
|
ShareCompat.IntentBuilder(context)
|
||||||
|
.setText(text)
|
||||||
|
.setType(TYPE_TEXT)
|
||||||
|
.setChooserTitle(R.string.share)
|
||||||
|
.startChooser()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareBackup(file: File) {
|
fun shareCbz(files: Collection<File>) {
|
||||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", file)
|
if (files.isEmpty()) {
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
return
|
||||||
intent.setDataAndType(uri, context.contentResolver.getType(uri))
|
}
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
val intentBuilder = ShareCompat.IntentBuilder(context)
|
||||||
val shareIntent =
|
.setType(TYPE_CBZ)
|
||||||
Intent.createChooser(intent, context.getString(R.string.share_s, file.name))
|
for (file in files) {
|
||||||
context.startActivity(shareIntent)
|
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", file)
|
||||||
|
intentBuilder.addStream(uri)
|
||||||
|
}
|
||||||
|
files.singleOrNull()?.let {
|
||||||
|
intentBuilder.setChooserTitle(context.getString(R.string.share_s, it.name))
|
||||||
|
} ?: run {
|
||||||
|
intentBuilder.setChooserTitle(R.string.share)
|
||||||
|
}
|
||||||
|
intentBuilder.startChooser()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareImage(uri: Uri) {
|
fun shareImage(uri: Uri) {
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
ShareCompat.IntentBuilder(context)
|
||||||
intent.setDataAndType(uri, context.contentResolver.getType(uri) ?: "image/*")
|
.setStream(uri)
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setType(context.contentResolver.getType(uri) ?: TYPE_IMAGE)
|
||||||
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_image))
|
.setChooserTitle(R.string.share_image)
|
||||||
context.startActivity(shareIntent)
|
.startChooser()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareText(text: String) {
|
fun shareText(text: String) {
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
ShareCompat.IntentBuilder(context)
|
||||||
intent.type = "text/plain"
|
.setText(text)
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
.setType(TYPE_TEXT)
|
||||||
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share))
|
.setChooserTitle(R.string.share)
|
||||||
context.startActivity(shareIntent)
|
.startChooser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
app/src/main/res/drawable/ic_delete.xml
Normal file
12
app/src/main/res/drawable/ic_delete.xml
Normal file
@@ -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="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000"
|
||||||
|
android:pathData="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z" />
|
||||||
|
</vector>
|
||||||
@@ -7,12 +7,12 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
style="?attr/textAppearanceTitleLarge"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:padding="16dp"
|
app:menu="@menu/opt_favourites_bs"
|
||||||
android:text="@string/add_to_favourites" />
|
app:title="@string/add_to_favourites" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView_categories"
|
android:id="@+id/recyclerView_categories"
|
||||||
@@ -22,19 +22,6 @@
|
|||||||
android:overScrollMode="never"
|
android:overScrollMode="never"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/item_category_checkable" />
|
tools:listitem="@layout/item_checkable_new" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView_add"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
|
||||||
android:background="?android:selectableItemBackground"
|
|
||||||
android:gravity="start|center_vertical"
|
|
||||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
|
||||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
|
||||||
android:text="@string/add_new_category"
|
|
||||||
android:textAppearance="?attr/textAppearanceButton"
|
|
||||||
android:textColor="?colorOnSurfaceVariant"
|
|
||||||
app:drawableEndCompat="@drawable/ic_list_add" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
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">
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_select_all"
|
|
||||||
android:icon="?actionModeSelectAllDrawable"
|
|
||||||
android:title="@android:string/selectAll"
|
|
||||||
app:showAsAction="ifRoom|withText" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_save"
|
android:id="@+id/action_save"
|
||||||
android:icon="@drawable/ic_save"
|
android:icon="@drawable/ic_save"
|
||||||
android:title="@string/save"
|
android:title="@string/save"
|
||||||
app:showAsAction="ifRoom|withText" />
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_select_all"
|
||||||
|
android:icon="?actionModeSelectAllDrawable"
|
||||||
|
android:title="@android:string/selectAll"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
24
app/src/main/res/menu/mode_favourites.xml
Normal file
24
app/src/main/res/menu/mode_favourites.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_share"
|
||||||
|
android:icon="?actionModeShareDrawable"
|
||||||
|
android:title="@string/share"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_remove"
|
||||||
|
android:icon="@drawable/ic_delete"
|
||||||
|
android:title="@string/remove"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_select_all"
|
||||||
|
android:icon="?actionModeSelectAllDrawable"
|
||||||
|
android:title="@android:string/selectAll"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
24
app/src/main/res/menu/mode_history.xml
Normal file
24
app/src/main/res/menu/mode_history.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_share"
|
||||||
|
android:icon="?actionModeShareDrawable"
|
||||||
|
android:title="@string/share"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_favourite"
|
||||||
|
android:icon="@drawable/ic_heart_outline"
|
||||||
|
android:title="@string/add_to_favourites"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_remove"
|
||||||
|
android:icon="@drawable/ic_delete"
|
||||||
|
android:title="@string/remove"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
23
app/src/main/res/menu/mode_local.xml
Normal file
23
app/src/main/res/menu/mode_local.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_share"
|
||||||
|
android:icon="?actionModeShareDrawable"
|
||||||
|
android:title="@string/share"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_remove"
|
||||||
|
android:icon="@drawable/ic_delete"
|
||||||
|
android:title="@string/delete"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_select_all"
|
||||||
|
android:icon="?actionModeSelectAllDrawable"
|
||||||
|
android:title="@android:string/selectAll"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
</menu>
|
||||||
24
app/src/main/res/menu/mode_remote.xml
Normal file
24
app/src/main/res/menu/mode_remote.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_share"
|
||||||
|
android:icon="?actionModeShareDrawable"
|
||||||
|
android:title="@string/share"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_favourite"
|
||||||
|
android:icon="@drawable/ic_heart_outline"
|
||||||
|
android:title="@string/add_to_favourites"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_save"
|
||||||
|
android:icon="@drawable/ic_save"
|
||||||
|
android:title="@string/save"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
13
app/src/main/res/menu/opt_favourites_bs.xml
Normal file
13
app/src/main/res/menu/opt_favourites_bs.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_create"
|
||||||
|
android:icon="@drawable/ic_list_add"
|
||||||
|
android:title="@string/add_new_category"
|
||||||
|
android:titleCondensed="@string/add"
|
||||||
|
app:showAsAction="ifRoom|withText" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_remove"
|
|
||||||
android:title="@string/remove" />
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_remove"
|
|
||||||
android:title="@string/remove" />
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_delete"
|
|
||||||
android:title="@string/delete" />
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<dimen name="list_footer_height_inner">36dp</dimen>
|
<dimen name="list_footer_height_inner">36dp</dimen>
|
||||||
<dimen name="list_footer_height_outer">48dp</dimen>
|
<dimen name="list_footer_height_outer">48dp</dimen>
|
||||||
<dimen name="screen_padding">16dp</dimen>
|
<dimen name="screen_padding">16dp</dimen>
|
||||||
|
<dimen name="selection_stroke_width">2dp</dimen>
|
||||||
|
|
||||||
<dimen name="search_suggestions_manga_height">124dp</dimen>
|
<dimen name="search_suggestions_manga_height">124dp</dimen>
|
||||||
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
|
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
|
||||||
|
|||||||
@@ -271,4 +271,7 @@
|
|||||||
<string name="suggestions_updating">Suggestions updating</string>
|
<string name="suggestions_updating">Suggestions updating</string>
|
||||||
<string name="suggestions_excluded_genres">Exclude genres</string>
|
<string name="suggestions_excluded_genres">Exclude genres</string>
|
||||||
<string name="suggestions_excluded_genres_summary">Specify genres that you do not want to see in the suggestions</string>
|
<string name="suggestions_excluded_genres_summary">Specify genres that you do not want to see in the suggestions</string>
|
||||||
|
<string name="text_delete_local_manga_batch">Delete selected items from device permanently?</string>
|
||||||
|
<string name="removal_completed">Removal completed</string>
|
||||||
|
<string name="batch_manga_save_confirm">Are you sure you want to download all selected manga with all its chapters? This action can consume a lot of traffic and storage</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -6,6 +6,11 @@
|
|||||||
<item name="android:tint">?colorControlNormal</item>
|
<item name="android:tint">?colorControlNormal</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Kotatsu.ActionMode" parent="Base.Widget.Material3.ActionMode">
|
||||||
|
<!--<item name="titleTextStyle">@style/TextAppearance.Kotatsu.ActionBar.Title</item>-->
|
||||||
|
<item name="theme">@style/ThemeOverlay.Kotatsu.ActionMode</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!--AlertDialog-->
|
<!--AlertDialog-->
|
||||||
|
|
||||||
<style name="ThemeOverlay.Kotatsu.MaterialAlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
<style name="ThemeOverlay.Kotatsu.MaterialAlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
||||||
@@ -15,6 +20,12 @@
|
|||||||
<item name="dialogCornerRadius">28dp</item>
|
<item name="dialogCornerRadius">28dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Bottom sheet -->
|
||||||
|
|
||||||
|
<style name="ThemeOverlay.Kotatsu.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
|
||||||
|
<item name="android:navigationBarColor">?colorSurfaceVariant</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Widget styles -->
|
<!-- Widget styles -->
|
||||||
|
|
||||||
<style name="Widget.Kotatsu.Tabs" parent="@style/Widget.Material3.TabLayout">
|
<style name="Widget.Kotatsu.Tabs" parent="@style/Widget.Material3.TabLayout">
|
||||||
@@ -92,6 +103,11 @@
|
|||||||
<item name="colorControlHighlight">@color/selector_overlay</item>
|
<item name="colorControlHighlight">@color/selector_overlay</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="ThemeOverlay.Kotatsu.ActionMode" parent="">
|
||||||
|
<item name="colorOnSurface">?colorPrimary</item>
|
||||||
|
<item name="colorControlNormal">?colorPrimary</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- TextAppearance -->
|
<!-- TextAppearance -->
|
||||||
|
|
||||||
<style name="TextAppearance.Widget.Menu" parent="TextAppearance.AppCompat.Menu">
|
<style name="TextAppearance.Widget.Menu" parent="TextAppearance.AppCompat.Menu">
|
||||||
@@ -111,6 +127,10 @@
|
|||||||
<item name="android:textAllCaps">true</item>
|
<item name="android:textAllCaps">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.Kotatsu.ActionBar.Title" parent="TextAppearance.Material3.TitleLarge">
|
||||||
|
<item name="android:textColor">?attr/colorPrimary</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Shapes -->
|
<!-- Shapes -->
|
||||||
|
|
||||||
<style name="ShapeAppearanceOverlay.Kotatsu.Cover" parent="">
|
<style name="ShapeAppearanceOverlay.Kotatsu.Cover" parent="">
|
||||||
|
|||||||
@@ -51,11 +51,13 @@
|
|||||||
<item name="android:enforceStatusBarContrast" tools:targetApi="Q">false</item>
|
<item name="android:enforceStatusBarContrast" tools:targetApi="Q">false</item>
|
||||||
<item name="android:itemTextAppearance">@style/TextAppearance.Widget.Menu</item>
|
<item name="android:itemTextAppearance">@style/TextAppearance.Widget.Menu</item>
|
||||||
<item name="alertDialogTheme">@style/ThemeOverlay.Kotatsu.MaterialAlertDialog</item>
|
<item name="alertDialogTheme">@style/ThemeOverlay.Kotatsu.MaterialAlertDialog</item>
|
||||||
|
<item name="bottomSheetDialogTheme">@style/ThemeOverlay.Kotatsu.BottomSheetDialog</item>
|
||||||
<item name="materialAlertDialogTheme">@style/ThemeOverlay.Kotatsu.MaterialAlertDialog</item>
|
<item name="materialAlertDialogTheme">@style/ThemeOverlay.Kotatsu.MaterialAlertDialog</item>
|
||||||
<item name="textAppearanceButton">@style/TextAppearance.Kotatsu.Button</item>
|
<item name="textAppearanceButton">@style/TextAppearance.Kotatsu.Button</item>
|
||||||
<item name="android:buttonStyle">?attr/borderlessButtonStyle</item>
|
<item name="android:buttonStyle">?attr/borderlessButtonStyle</item>
|
||||||
<item name="android:backgroundDimAmount">0.32</item>
|
<item name="android:backgroundDimAmount">0.32</item>
|
||||||
<item name="windowActionModeOverlay">true</item>
|
<item name="windowActionModeOverlay">true</item>
|
||||||
|
<item name="actionModeStyle">@style/Widget.Kotatsu.ActionMode</item>
|
||||||
<item name="actionModeCloseDrawable">@drawable/abc_ic_clear_material</item>
|
<item name="actionModeCloseDrawable">@drawable/abc_ic_clear_material</item>
|
||||||
<item name="actionModeWebSearchDrawable">@drawable/abc_ic_search_api_material</item>
|
<item name="actionModeWebSearchDrawable">@drawable/abc_ic_search_api_material</item>
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Kotatsu</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Kotatsu</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user