Merge branch 'feature/multiselect' into devel

This commit is contained in:
Koitharu
2022-04-09 18:39:25 +03:00
52 changed files with 901 additions and 337 deletions

View File

@@ -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 &&

View File

@@ -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
} }

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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 }

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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)
} }

View File

@@ -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()
} }
} }

View File

@@ -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)

View File

@@ -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> {

View File

@@ -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()

View File

@@ -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)
} }
} }

View File

@@ -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
}
}
} }

View File

@@ -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 {

View File

@@ -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)
} }
} }
} }

View File

@@ -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

View File

@@ -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()

View File

@@ -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()
} }
} }

View File

@@ -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() {

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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?) {

View File

@@ -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)
} }
} }

View File

@@ -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
)
}
} }

View File

@@ -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)
} }

View File

@@ -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"

View File

@@ -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"

View File

@@ -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()

View File

@@ -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>()

View File

@@ -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)
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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()
} }
} }

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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="">

View File

@@ -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>