Use Compose on Clear Database screen (#7639)

This commit is contained in:
Andreas 2022-07-30 17:51:47 +02:00 committed by GitHub
parent 4774deb1ef
commit 99ac30e59f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 354 additions and 325 deletions

View File

@ -2,12 +2,12 @@ package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.model.SourceWithCount
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import eu.kanade.tachiyomi.source.Source as LoadedSource
class SourceRepositoryImpl(
private val sourceManager: SourceManager,
@ -40,12 +40,12 @@ class SourceRepositoryImpl(
}
}
override fun getSourcesWithNonLibraryManga(): Flow<List<Pair<LoadedSource, Long>>> {
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {
val sourceIdWithNonLibraryManga = handler.subscribeToList { mangasQueries.getSourceIdsWithNonLibraryManga() }
return sourceIdWithNonLibraryManga.map { sourceId ->
sourceId.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
source to count
SourceWithCount(sourceMapper(source), count)
}
}
}

View File

@ -1,14 +1,14 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.model.SourceWithCount
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.flow.Flow
class GetSourcesWithNonLibraryManga(
private val repository: SourceRepository,
) {
fun subscribe(): Flow<List<Pair<Source, Long>>> {
fun subscribe(): Flow<List<SourceWithCount>> {
return repository.getSourcesWithNonLibraryManga()
}
}

View File

@ -0,0 +1,13 @@
package eu.kanade.domain.source.model
data class SourceWithCount(
val source: Source,
val count: Long,
) {
val id: Long
get() = source.id
val name: String
get() = source.name
}

View File

@ -1,8 +1,8 @@
package eu.kanade.domain.source.repository
import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.model.SourceWithCount
import kotlinx.coroutines.flow.Flow
import eu.kanade.tachiyomi.source.Source as LoadedSource
interface SourceRepository {
@ -12,5 +12,5 @@ interface SourceRepository {
fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
fun getSourcesWithNonLibraryManga(): Flow<List<Pair<LoadedSource, Long>>>
fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>>
}

View File

@ -0,0 +1,57 @@
package eu.kanade.presentation.more.settings.database
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.database.components.ClearDatabaseContent
import eu.kanade.presentation.more.settings.database.components.ClearDatabaseDeleteDialog
import eu.kanade.presentation.more.settings.database.components.ClearDatabaseFloatingActionButton
import eu.kanade.presentation.more.settings.database.components.ClearDatabaseToolbar
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.setting.database.ClearDatabasePresenter
import eu.kanade.tachiyomi.util.system.toast
@Composable
fun ClearDatabaseScreen(
presenter: ClearDatabasePresenter,
navigateUp: () -> Unit,
) {
val context = LocalContext.current
Scaffold(
topBar = {
ClearDatabaseToolbar(
state = presenter,
navigateUp = navigateUp,
onClickSelectAll = { presenter.selectAll() },
onClickInvertSelection = { presenter.invertSelection() },
)
},
floatingActionButton = {
ClearDatabaseFloatingActionButton(
isVisible = presenter.selection.isNotEmpty(),
onClickDelete = {
presenter.dialog = ClearDatabasePresenter.Dialog.Delete(presenter.selection)
},
)
},
) { paddingValues ->
ClearDatabaseContent(
state = presenter,
contentPadding = paddingValues,
onClickSelection = { source ->
presenter.toggleSelection(source)
},
)
}
if (presenter.dialog is ClearDatabasePresenter.Dialog.Delete) {
ClearDatabaseDeleteDialog(
onDismissRequest = { presenter.dialog = null },
onDelete = {
presenter.removeMangaBySourceId((presenter.dialog as ClearDatabasePresenter.Dialog.Delete).sourceIds)
presenter.clearSelection()
context.toast(R.string.clear_database_completed)
},
)
}
}

View File

@ -0,0 +1,28 @@
package eu.kanade.presentation.more.settings.database
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.source.model.SourceWithCount
import eu.kanade.tachiyomi.ui.setting.database.ClearDatabasePresenter
@Stable
interface ClearDatabaseState {
val items: List<SourceWithCount>
val selection: List<Long>
val isEmpty: Boolean
var dialog: ClearDatabasePresenter.Dialog?
}
fun ClearDatabaseState(): ClearDatabaseState {
return ClearDatabaseStateImpl()
}
class ClearDatabaseStateImpl : ClearDatabaseState {
override var items: List<SourceWithCount> by mutableStateOf(emptyList())
override var selection: List<Long> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
override var dialog: ClearDatabasePresenter.Dialog? by mutableStateOf(null)
}

View File

@ -0,0 +1,41 @@
package eu.kanade.presentation.more.settings.database.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.more.settings.database.ClearDatabaseState
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
@Composable
fun ClearDatabaseContent(
state: ClearDatabaseState,
contentPadding: PaddingValues,
onClickSelection: (Source) -> Unit,
) {
Crossfade(targetState = state.isEmpty.not()) { _state ->
when (_state) {
true -> FastScrollLazyColumn(
contentPadding = contentPadding + WindowInsets.navigationBars.asPaddingValues(),
) {
items(state.items) { sourceWithCount ->
ClearDatabaseItem(
source = sourceWithCount.source,
count = sourceWithCount.count,
isSelected = state.selection.contains(sourceWithCount.id),
onClickSelect = { onClickSelection(sourceWithCount.source) },
)
}
}
false -> EmptyScreen(message = stringResource(id = R.string.database_clean))
}
}
}

View File

@ -0,0 +1,31 @@
package eu.kanade.presentation.more.settings.database.components
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.TextButton
import eu.kanade.tachiyomi.R
@Composable
fun ClearDatabaseDeleteDialog(
onDismissRequest: () -> Unit,
onDelete: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDelete) {
Text(text = stringResource(id = android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(id = android.R.string.cancel))
}
},
text = {
Text(text = stringResource(id = R.string.clear_database_confirmation))
},
)
}

View File

@ -0,0 +1,38 @@
package eu.kanade.presentation.more.settings.database.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.tachiyomi.R
@Composable
fun ClearDatabaseFloatingActionButton(
isVisible: Boolean,
onClickDelete: () -> Unit,
) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
modifier = Modifier.navigationBarsPadding(),
text = {
Text(text = stringResource(id = R.string.action_delete))
},
icon = {
Icon(Icons.Outlined.Delete, contentDescription = "")
},
onClick = onClickDelete,
)
}
}

View File

@ -0,0 +1,53 @@
package eu.kanade.presentation.more.settings.database.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.tachiyomi.R
@Composable
fun ClearDatabaseItem(
source: Source,
count: Long,
isSelected: Boolean,
onClickSelect: () -> Unit,
) {
Row(
modifier = Modifier
.selectedBackground(isSelected)
.clickable(onClick = onClickSelect)
.padding(horizontal = 8.dp)
.height(56.dp),
verticalAlignment = Alignment.CenterVertically,
) {
SourceIcon(source = source)
Column(
modifier = Modifier
.padding(start = 8.dp)
.weight(1f),
) {
Text(
text = source.nameWithLanguage,
style = MaterialTheme.typography.bodyMedium,
)
Text(text = stringResource(id = R.string.clear_database_source_item_count, count))
}
Checkbox(
checked = isSelected,
onCheckedChange = { onClickSelect() },
)
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.presentation.more.settings.database.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.more.settings.database.ClearDatabaseState
import eu.kanade.tachiyomi.R
@Composable
fun ClearDatabaseToolbar(
state: ClearDatabaseState,
navigateUp: () -> Unit,
onClickSelectAll: () -> Unit,
onClickInvertSelection: () -> Unit,
) {
AppBar(
title = stringResource(id = R.string.pref_clear_database),
navigateUp = navigateUp,
actions = {
if (state.isEmpty.not()) {
AppBarActions(
actions = listOf(
AppBar.Action(
title = stringResource(id = R.string.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onClickSelectAll,
),
AppBar.Action(
title = stringResource(id = R.string.action_select_all),
icon = Icons.Outlined.FlipToBack,
onClick = onClickInvertSelection,
),
),
)
}
},
)
}

View File

@ -1,171 +1,20 @@
package eu.kanade.tachiyomi.ui.setting.database
import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.Payload
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ClearDatabaseControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.system.toast
import androidx.compose.runtime.Composable
import eu.kanade.presentation.more.settings.database.ClearDatabaseScreen
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
class ClearDatabaseController :
NucleusController<ClearDatabaseControllerBinding, ClearDatabasePresenter>(),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnUpdateListener,
FabController {
private var recycler: RecyclerView? = null
private var adapter: FlexibleAdapter<ClearDatabaseSourceItem>? = null
private var menu: Menu? = null
private var actionFab: ExtendedFloatingActionButton? = null
init {
setHasOptionsMenu(true)
}
override fun createBinding(inflater: LayoutInflater): ClearDatabaseControllerBinding {
return ClearDatabaseControllerBinding.inflate(inflater)
}
class ClearDatabaseController : FullComposeController<ClearDatabasePresenter>() {
override fun createPresenter(): ClearDatabasePresenter {
return ClearDatabasePresenter()
}
override fun getTitle(): String? {
return activity?.getString(R.string.pref_clear_database)
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
binding.recycler.adapter = adapter
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
adapter?.fastScroller = binding.fastScroller
recycler = binding.recycler
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.generic_selection, menu)
this.menu = menu
menu.forEach { menuItem -> menuItem.isVisible = (adapter?.itemCount ?: 0) > 0 }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val adapter = adapter ?: return false
when (item.itemId) {
R.id.action_select_all -> adapter.selectAll()
R.id.action_select_inverse -> {
adapter.currentItems.forEachIndexed { index, _ ->
adapter.toggleSelection(index)
}
}
}
updateFab()
adapter.notifyItemRangeChanged(0, adapter.itemCount, Payload.SELECTION)
return super.onOptionsItemSelected(item)
}
override fun onUpdateEmptyView(size: Int) {
if (size > 0) {
binding.emptyView.hide()
} else {
binding.emptyView.show(activity!!.getString(R.string.database_clean))
}
menu?.forEach { menuItem -> menuItem.isVisible = size > 0 }
}
override fun onItemClick(view: View?, position: Int): Boolean {
val adapter = adapter ?: return false
adapter.toggleSelection(position)
adapter.notifyItemChanged(position, Payload.SELECTION)
updateFab()
return true
}
fun setItems(items: List<ClearDatabaseSourceItem>) {
adapter?.updateDataSet(items)
}
override fun configureFab(fab: ExtendedFloatingActionButton) {
fab.setIconResource(R.drawable.ic_delete_24dp)
fab.setText(R.string.action_delete)
fab.hide()
fab.setOnClickListener {
val ctrl = ClearDatabaseSourcesDialog()
ctrl.targetController = this
ctrl.showDialog(router)
}
actionFab = fab
}
private fun updateFab() {
val adapter = adapter ?: return
if (adapter.selectedItemCount > 0) {
actionFab?.show()
} else {
actionFab?.hide()
}
}
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
actionFab?.setOnClickListener(null)
actionFab = null
}
class ClearDatabaseSourcesDialog : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(activity!!)
.setMessage(R.string.clear_database_confirmation)
.setPositiveButton(android.R.string.ok) { _, _ ->
(targetController as? ClearDatabaseController)?.clearDatabaseForSelectedSources()
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun clearDatabaseForSelectedSources() {
val adapter = adapter ?: return
val selectedSourceIds = adapter.selectedPositions.mapNotNull { position ->
adapter.getItem(position)?.source?.id
}
presenter.clearDatabaseForSourceIds(selectedSourceIds)
actionFab!!.isVisible = false
adapter.clearSelection()
adapter.notifyDataSetChanged()
activity?.toast(R.string.clear_database_completed)
@Composable
override fun ComposeContent() {
ClearDatabaseScreen(
presenter = presenter,
navigateUp = { router.popCurrentController() },
)
}
}

View File

@ -2,18 +2,21 @@ package eu.kanade.tachiyomi.ui.setting.database
import android.os.Bundle
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.more.settings.database.ClearDatabaseState
import eu.kanade.presentation.more.settings.database.ClearDatabaseStateImpl
import eu.kanade.tachiyomi.Database
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.flow.collectLatest
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ClearDatabasePresenter(
private val state: ClearDatabaseStateImpl = ClearDatabaseState() as ClearDatabaseStateImpl,
private val database: Database = Injekt.get(),
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get(),
) : BasePresenter<ClearDatabaseController>() {
) : BasePresenter<ClearDatabaseController>(), ClearDatabaseState by state {
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
@ -21,17 +24,39 @@ class ClearDatabasePresenter(
presenterScope.launchIO {
getSourcesWithNonLibraryManga.subscribe()
.collectLatest { list ->
val items = list
.map { (source, count) -> ClearDatabaseSourceItem(source, count) }
.sortedBy { it.source.name }
withUIContext { view?.setItems(items) }
state.items = list.sortedBy { it.name }
}
}
}
fun clearDatabaseForSourceIds(sources: List<Long>) {
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(sources)
fun removeMangaBySourceId(sourceIds: List<Long>) {
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(sourceIds)
database.historyQueries.removeResettedHistory()
}
fun toggleSelection(source: Source) {
val mutableList = state.selection.toMutableList()
if (mutableList.contains(source.id)) {
mutableList.remove(source.id)
} else {
mutableList.add(source.id)
}
state.selection = mutableList
}
fun clearSelection() {
state.selection = emptyList()
}
fun selectAll() {
state.selection = state.items.map { it.id }
}
fun invertSelection() {
state.selection = state.items.map { it.id }.filterNot { it in state.selection }
}
sealed class Dialog {
data class Delete(val sourceIds: List<Long>) : Dialog()
}
}

View File

@ -1,48 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.database
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ClearDatabaseSourceItemBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.icon
data class ClearDatabaseSourceItem(val source: Source, private val mangaCount: Long) : AbstractFlexibleItem<ClearDatabaseSourceItem.Holder>() {
override fun getLayoutRes(): Int {
return R.layout.clear_database_source_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?, holder: Holder?, position: Int, payloads: MutableList<Any>?) {
holder?.bind(source, mangaCount)
}
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
private val binding = ClearDatabaseSourceItemBinding.bind(view)
fun bind(source: Source, count: Long) {
binding.title.text = source.toString()
binding.description.text = itemView.context.getString(R.string.clear_database_source_item_count, count)
itemView.post {
when {
source.icon() != null && source.id != LocalSource.ID ->
binding.thumbnail.setImageDrawable(source.icon())
else -> binding.thumbnail.setImageResource(R.mipmap.ic_local_source)
}
}
binding.checkbox.isChecked = (bindingAdapter as FlexibleAdapter<*>).isSelected(bindingAdapterPosition)
}
}
}

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="multipleChoice"
android:clipToPadding="false"
android:paddingBottom="@dimen/fab_list_padding"
tools:listitem="@layout/clear_database_source_item" />
<eu.kanade.tachiyomi.widget.MaterialFastScroll
android:id="@+id/fast_scroller"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
app:fastScrollerBubbleEnabled="false"
tools:visibility="visible" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>

View File

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@drawable/list_item_selector_background"
android:paddingStart="8dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingHorizontal="8dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/title"
style="?attr/textAppearanceBodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="0dp"
android:paddingEnd="8dp"
android:ellipsize="middle"
android:maxLines="1"
app:layout_constraintStart_toEndOf="@+id/thumbnail"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/description"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Source Name (LN)" />
<TextView
android:id="@+id/description"
style="?attr/textAppearanceBodySmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintStart_toEndOf="@+id/thumbnail"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="999 non-library manga in database" />
<CheckBox
android:id="@+id/checkbox"
android:layout_width="0dp"
android:layout_height="match_parent"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:clickable="false"
android:background="@android:color/transparent"
android:longClickable="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:2"
/>
</androidx.constraintlayout.widget.ConstraintLayout>