Convert mass migration to compose

This commit is contained in:
Jobobby04 2022-11-26 15:25:03 -05:00
parent aaddb4bf00
commit e696b95330
6 changed files with 423 additions and 427 deletions

View File

@ -1,10 +1,13 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.content.Context
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import java.text.DecimalFormat
import kotlin.coroutines.CoroutineContext
class MigratingManga(
@ -12,9 +15,6 @@ class MigratingManga(
val chapterInfo: ChapterInfo,
val sourcesString: String,
parentContext: CoroutineContext,
val getManga: suspend (SearchResult.Result) -> Manga?,
val getChapterInfo: suspend (SearchResult.Result) -> ChapterInfo,
val getSourceName: (Manga) -> String,
) {
val migrationScope = CoroutineScope(parentContext + SupervisorJob() + Dispatchers.Default)
@ -32,10 +32,19 @@ class MigratingManga(
data class ChapterInfo(
val latestChapter: Float?,
val chapterCount: Int,
)
fun toModal(): MigrationProcessItem {
// Create the model object.
return MigrationProcessItem(this)
) {
fun getFormattedLatestChapter(context: Context): String {
return if (latestChapter != null && latestChapter > 0f) {
context.getString(
R.string.latest_,
DecimalFormat("#.#").format(latestChapter),
)
} else {
context.getString(
R.string.latest_,
context.getString(R.string.unknown),
)
}
}
}
}

View File

@ -1,24 +1,70 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.graphics.Color
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.graphics.ColorUtils
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.CopyAll
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.MigrationListControllerBinding
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.MigrationMangaDialog
@ -26,14 +72,13 @@ import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationContr
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.getParcelableCompat
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.saket.cascade.CascadeDropdownMenu
class MigrationListController(bundle: Bundle? = null) :
NucleusController<MigrationListControllerBinding, MigrationListPresenter>(bundle) {
FullComposeController<MigrationListPresenter>(bundle) {
constructor(config: MigrationProcedureConfig) : this(
bundleOf(
@ -41,63 +86,337 @@ class MigrationListController(bundle: Bundle? = null) :
),
)
init {
setHasOptionsMenu(true)
}
private var adapter: MigrationProcessAdapter? = null
val config = args.getParcelableCompat<MigrationProcedureConfig>(CONFIG_EXTRA)
private var selectedMangaId: Long? = null
private var manualMigrations = 0
override fun getTitle(): String {
val notFinished = presenter.migratingItems.value.count {
it.searchResult.value != SearchResult.Searching
}
val total = presenter.migratingItems.value.size
return activity?.getString(R.string.migration) + " ($notFinished/$total)"
}
override fun createPresenter(): MigrationListPresenter {
return MigrationListPresenter(config!!)
}
override fun createBinding(inflater: LayoutInflater) = MigrationListControllerBinding.inflate(inflater)
@Composable
override fun ComposeContent() {
val items by presenter.migratingItems.collectAsState()
val migrationDone by presenter.migrationDone.collectAsState()
val unfinishedCount by presenter.unfinishedCount.collectAsState()
Scaffold(
topBar = { scrollBehavior ->
val titleString = stringResource(R.string.migration)
val title by produceState(initialValue = titleString, items, unfinishedCount, titleString) {
withIOContext {
value = "$titleString ($unfinishedCount/${items.size})"
}
}
AppBar(
title = title,
actions = {
IconButton(
onClick = { openMigrationDialog(true) },
enabled = migrationDone,
) {
Icon(
imageVector = if (items.size == 1) Icons.Outlined.ContentCopy else Icons.Outlined.CopyAll,
contentDescription = stringResource(R.string.copy),
)
}
IconButton(
onClick = { openMigrationDialog(false) },
enabled = migrationDone,
) {
Icon(
imageVector = if (items.size == 1) Icons.Outlined.Done else Icons.Outlined.DoneAll,
contentDescription = stringResource(R.string.migrate),
)
}
},
scrollBehavior = scrollBehavior,
)
},
) { contentPadding ->
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
items(items, key = { it.manga.id }) { migrationItem ->
Row(
Modifier
.fillMaxWidth()
.animateItemPlacement()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
val result by migrationItem.searchResult.collectAsState()
MigrationItem(
modifier = Modifier
.padding(top = 8.dp)
.weight(1f),
manga = migrationItem.manga,
sourcesString = migrationItem.sourcesString,
chapterInfo = migrationItem.chapterInfo,
onClick = {
router.pushController(
MangaController(
migrationItem.manga.id,
true,
),
)
},
)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
Icon(
Icons.Outlined.ArrowForward,
contentDescription = stringResource(R.string.migrating_to),
modifier = Modifier.weight(0.2f),
)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
MigrationItemResult(
modifier = Modifier
.padding(top = 8.dp)
.weight(1f),
migrationItem = migrationItem,
result = result,
)
MigrationActionIcon(
modifier = Modifier
.weight(0.2f),
result = result,
skipManga = { presenter.removeManga(migrationItem.manga.id) },
searchManually = {
val manga = migrationItem.manga
selectedMangaId = manga.id
val sources = presenter.getMigrationSources()
val validSources = if (sources.size == 1) {
sources
} else {
sources.filter { it.id != manga.source }
}
val searchController = SearchController(manga, validSources)
searchController.targetController = this@MigrationListController
router.pushController(searchController)
},
migrateNow = {
migrateManga(migrationItem.manga.id, false)
manualMigrations++
},
copyNow = {
migrateManga(migrationItem.manga.id, true)
manualMigrations++
},
)
}
}
}
}
setTitle()
adapter = MigrationProcessAdapter(this)
binding.recycler.adapter = adapter
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
presenter.migratingItems
.onEach {
adapter?.updateDataSet(it.map { it.toModal() })
}
.launchIn(viewScope)
}
fun updateCount() {
if (router.backstack.lastOrNull()?.controller == this@MigrationListController) {
setTitle()
}
}
private fun enableButtons() {
activity?.invalidateOptionsMenu()
@Composable
fun MigrationItem(
modifier: Modifier,
manga: Manga,
sourcesString: String,
chapterInfo: MigratingManga.ChapterInfo,
onClick: () -> Unit,
) {
Column(
modifier
.widthIn(max = 150.dp)
.fillMaxWidth()
.clip(MaterialTheme.shapes.small)
.clickable(onClick = onClick)
.padding(4.dp),
) {
val context = LocalContext.current
Box(
Modifier.fillMaxWidth()
.aspectRatio(MangaCover.Book.ratio),
) {
MangaCover.Book(
modifier = Modifier
.fillMaxWidth(),
data = manga,
)
Box(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
.background(
Brush.verticalGradient(
0f to Color.Transparent,
1f to Color(0xAA000000),
),
)
.fillMaxHeight(0.33f)
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
Text(
modifier = Modifier
.padding(8.dp)
.align(Alignment.BottomStart),
text = manga.title.ifBlank { stringResource(R.string.unknown) },
fontSize = 12.sp,
lineHeight = 18.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleSmall.copy(
color = Color.White,
shadow = Shadow(
color = Color.Black,
blurRadius = 4f,
),
),
)
BadgeGroup(modifier = Modifier.padding(4.dp)) {
Badge(text = "${chapterInfo.chapterCount}")
}
}
Text(
text = sourcesString,
modifier = Modifier.padding(top = 4.dp, bottom = 1.dp, start = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.titleSmall,
)
val formattedLatestChapter by produceState(initialValue = "") {
value = withIOContext {
chapterInfo.getFormattedLatestChapter(context)
}
}
Text(
text = formattedLatestChapter,
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp, start = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.bodyMedium,
)
}
}
@Composable
fun MigrationActionIcon(
modifier: Modifier,
result: SearchResult,
skipManga: () -> Unit,
searchManually: () -> Unit,
migrateNow: () -> Unit,
copyNow: () -> Unit,
) {
var moreExpanded by remember { mutableStateOf(false) }
val closeMenu = { moreExpanded = false }
Box(modifier) {
if (result is SearchResult.Searching) {
IconButton(onClick = skipManga) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_stop),
)
}
} else if (result is SearchResult.Result || result is SearchResult.NotFound) {
IconButton(onClick = { moreExpanded = !moreExpanded }) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
)
}
CascadeDropdownMenu(
expanded = moreExpanded,
onDismissRequest = closeMenu,
offset = DpOffset(8.dp, (-56).dp),
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.action_search_manually)) },
onClick = {
searchManually()
closeMenu()
},
)
DropdownMenuItem(
text = { Text(stringResource(R.string.action_skip_manga)) },
onClick = {
skipManga()
closeMenu()
},
)
if (result is SearchResult.Result) {
DropdownMenuItem(
text = { Text(stringResource(R.string.action_migrate_now)) },
onClick = {
migrateNow()
closeMenu()
},
)
DropdownMenuItem(
text = { Text(stringResource(R.string.action_copy_now)) },
onClick = {
copyNow()
closeMenu()
},
)
}
}
}
}
}
@Composable
fun MigrationItemResult(modifier: Modifier, migrationItem: MigratingManga, result: SearchResult) {
Box(modifier) {
when (result) {
SearchResult.Searching -> Box(
modifier = Modifier
.widthIn(max = 150.dp)
.fillMaxWidth()
.aspectRatio(MangaCover.Book.ratio),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
SearchResult.NotFound -> Image(
painter = rememberResourceBitmapPainter(id = R.drawable.cover_error),
contentDescription = null,
modifier = Modifier
.matchParentSize()
.clip(RoundedCornerShape(4.dp)),
contentScale = ContentScale.Crop,
)
is SearchResult.Result -> {
val item by produceState<Triple<Manga, MigratingManga.ChapterInfo, String>?>(
initialValue = null,
migrationItem,
result,
) {
value = withIOContext {
val manga = presenter.getManga(result) ?: return@withIOContext null
Triple(
manga,
presenter.getChapterInfo(result),
presenter.getSourceName(manga),
)
}
}
if (item != null) {
val (manga, chapterInfo, source) = item!!
MigrationItem(
modifier = Modifier,
manga = manga,
sourcesString = source,
chapterInfo = chapterInfo,
onClick = {
router.pushController(
MangaController(
manga.id,
true,
),
)
},
)
}
}
}
}
}
private fun noMigration() {
@ -116,36 +435,6 @@ class MigrationListController(bundle: Bundle? = null) :
}
}
fun onMenuItemClick(mangaId: Long, item: MenuItem) {
when (item.itemId) {
R.id.action_search_manually -> {
val manga = presenter.migratingItems.value
.find { it.manga.id == mangaId }
?.manga
?: return
selectedMangaId = mangaId
val sources = presenter.getMigrationSources()
val validSources = if (sources.size == 1) {
sources
} else {
sources.filter { it.id != manga.source }
}
val searchController = SearchController(manga, validSources)
searchController.targetController = this@MigrationListController
router.pushController(searchController)
}
R.id.action_skip -> presenter.removeManga(mangaId)
R.id.action_migrate_now -> {
migrateManga(mangaId, false)
manualMigrations++
}
R.id.action_copy_now -> {
migrateManga(mangaId, true)
manualMigrations++
}
}
}
fun useMangaForMigration(manga: Manga, source: Source) {
presenter.useMangaForMigration(manga, source, selectedMangaId ?: return)
}
@ -167,9 +456,7 @@ class MigrationListController(bundle: Bundle? = null) :
}
fun sourceFinished() {
updateCount()
if (presenter.migratingItems.value.isEmpty()) noMigration()
if (presenter.allMangasDone()) enableButtons()
}
fun navigateOut(manga: Manga?) {
@ -198,61 +485,15 @@ class MigrationListController(bundle: Bundle? = null) :
return true
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.migration_list, menu)
}
override fun onPrepareOptionsMenu(menu: Menu) {
// Initialize menu items.
val allMangasDone = presenter.allMangasDone()
val menuCopy = menu.findItem(R.id.action_copy_manga)
val menuMigrate = menu.findItem(R.id.action_migrate_manga)
if (presenter.migratingItems.value.size == 1) {
menuMigrate.icon = VectorDrawableCompat.create(
resources!!,
R.drawable.ic_done_24dp,
null,
)
}
val tintColor = activity?.getResourceColor(R.attr.colorOnSurface) ?: Color.WHITE
val color = if (allMangasDone) {
tintColor
} else {
ColorUtils.setAlphaComponent(tintColor, 127)
}
menuCopy.setIconTint(allMangasDone, color)
menuMigrate.setIconTint(allMangasDone, color)
}
private fun MenuItem.setIconTint(enabled: Boolean, color: Int) {
icon?.mutate()
icon?.setTint(color)
isEnabled = enabled
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
private fun openMigrationDialog(copy: Boolean) {
val totalManga = presenter.migratingItems.value.size
val mangaSkipped = presenter.mangasSkipped()
when (item.itemId) {
R.id.action_copy_manga -> MigrationMangaDialog(
this,
true,
totalManga,
mangaSkipped,
).showDialog(router)
R.id.action_migrate_manga -> MigrationMangaDialog(
this,
false,
totalManga,
mangaSkipped,
).showDialog(router)
else -> return super.onOptionsItemSelected(item)
}
return true
MigrationMangaDialog(
this,
copy,
totalManga,
mangaSkipped,
).showDialog(router)
}
companion object {

View File

@ -83,6 +83,8 @@ class MigrationListPresenter(
private set
val migratingItems = MutableStateFlow<List<MigratingManga>>(emptyList())
val migrationDone = MutableStateFlow(false)
val unfinishedCount = MutableStateFlow(0)
val hideNotFound = preferences.hideNotFoundMigration().get()
@ -108,9 +110,6 @@ class MigrationListPresenter(
},
),
parentContext = presenterScope.coroutineContext,
getManga = ::getManga,
getChapterInfo = ::getChapterInfo,
getSourceName = ::getSourceName,
)
}
}
@ -124,15 +123,15 @@ class MigrationListPresenter(
}
}
private suspend fun getManga(result: SearchResult.Result) = getManga.await(result.id)
private suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id)
private suspend fun getChapterInfo(id: Long) = getChapterByMangaId.await(id).let { chapters ->
suspend fun getManga(result: SearchResult.Result) = getManga.await(result.id)
suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id)
suspend fun getChapterInfo(id: Long) = getChapterByMangaId.await(id).let { chapters ->
MigratingManga.ChapterInfo(
latestChapter = chapters.maxOfOrNull { it.chapterNumber },
chapterCount = chapters.size,
)
}
private fun getSourceName(manga: Manga) = sourceManager.getOrStub(manga.source).getNameForMangaInfo(null)
fun getSourceName(manga: Manga) = sourceManager.getOrStub(manga.source).getNameForMangaInfo(null)
fun getMigrationSources() = preferences.migrationSources().get().split("/").mapNotNull {
val value = it.toLongOrNull() ?: return@mapNotNull null
@ -141,6 +140,7 @@ class MigrationListPresenter(
private suspend fun runMigrations(mangas: List<MigratingManga>) {
throttleManager.resetThrottle()
unfinishedCount.value = mangas.size
val useSourceWithMost = preferences.useSourceWithMost().get()
val useSmartSearch = preferences.smartMigration().get()
@ -269,13 +269,21 @@ class MigrationListPresenter(
if (result == null && hideNotFound) {
removeManga(manga)
}
withUIContext {
view?.sourceFinished()
}
sourceFinished()
}
}
}
private suspend fun sourceFinished() {
unfinishedCount.value = migratingItems.value.count {
it.searchResult.value != SearchResult.Searching
}
if (allMangasDone()) {
migrationDone.value = true
}
withUIContext { view?.sourceFinished() }
}
fun allMangasDone() = migratingItems.value.all { it.searchResult.value != SearchResult.Searching } &&
migratingItems.value.any { it.searchResult.value is SearchResult.Result }
@ -292,8 +300,8 @@ class MigrationListPresenter(
// Update chapters read
if (MigrationFlags.hasChapters(flags)) {
val prevMangaChapters = getChapterByMangaId.await(prevManga.id)
val maxChapterRead =
prevMangaChapters.filter(Chapter::read).maxOfOrNull(Chapter::chapterNumber)
val maxChapterRead = prevMangaChapters.filter(Chapter::read)
.maxOfOrNull(Chapter::chapterNumber)
val dbChapters = getChapterByMangaId.await(manga.id)
val prevHistoryList = getHistoryByMangaId.await(prevManga.id)
@ -489,16 +497,12 @@ class MigrationListPresenter(
if (migratingItems.value.size == 1) {
item.searchResult.value = SearchResult.NotFound
item.migrationScope.cancel()
withUIContext {
view?.sourceFinished()
}
sourceFinished()
return@launchIO
}
removeManga(item)
item.migrationScope.cancel()
withUIContext {
view?.sourceFinished()
}
sourceFinished()
}
}

View File

@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import eu.davidea.flexibleadapter.FlexibleAdapter
class MigrationProcessAdapter(
val controller: MigrationListController,
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true)

View File

@ -1,210 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.view.View
import android.widget.PopupMenu
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import coil.dispose
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.MigrationMangaCardBinding
import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.ChapterInfo
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.view.loadAutoPause
import eu.kanade.tachiyomi.util.view.setVectorCompat
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
import java.text.DecimalFormat
import java.util.concurrent.CopyOnWriteArrayList
class MigrationProcessHolder(
view: View,
private val adapter: MigrationProcessAdapter,
) : FlexibleViewHolder(view, adapter) {
private var item: MigrationProcessItem? = null
private val binding = MigrationProcessItemBinding.bind(view)
private val jobs = CopyOnWriteArrayList<Job>()
init {
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
// correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown.
binding.migrationMenu.setOnClickListener { it.post { showPopupMenu(it) } }
binding.skipManga.setOnClickListener { it.post { adapter.controller.removeManga(item?.manga?.manga?.id ?: return@post) } }
}
fun bind(item: MigrationProcessItem) {
this.item = item
jobs.removeAll { it.cancel(); true }
jobs += adapter.controller.viewScope.launchUI {
val migrateManga = item.manga
val manga = migrateManga.manga
binding.migrationMenu.setVectorCompat(
R.drawable.ic_more_24dp,
R.attr.colorOnPrimary,
)
binding.skipManga.setVectorCompat(
R.drawable.ic_close_24dp,
R.attr.colorOnPrimary,
)
binding.migrationMenu.isInvisible = true
binding.skipManga.isVisible = true
binding.migrationMangaCardTo.resetManga()
binding.migrationMangaCardFrom.attachManga(manga, item.manga.sourcesString, item.manga.chapterInfo)
jobs += binding.migrationMangaCardFrom.root.clicks()
.onEach {
adapter.controller.router.pushController(
MangaController(
manga.id,
true,
),
)
}
.launchIn(adapter.controller.viewScope)
/*launchUI {
item.manga.progress.asFlow().collect { (max, progress) ->
withUIContext {
migration_manga_card_to.search_progress.let { progressBar ->
progressBar.max = max
progressBar.progress = progress
}
}
}
}*/
jobs += migrateManga.searchResult
.onEach { searchResult ->
this@MigrationProcessHolder.logcat { (searchResult to (migrateManga.manga.id to this@MigrationProcessHolder.item?.manga?.manga?.id)).toString() }
if (migrateManga.manga.id != this@MigrationProcessHolder.item?.manga?.manga?.id ||
searchResult == SearchResult.Searching
) {
return@onEach
}
val resultManga = withIOContext {
(searchResult as? SearchResult.Result)
?.let { migrateManga.getManga(it) }
}
if (resultManga != null) {
val (sourceName, latestChapter) = withIOContext {
val sourceNameAsync = async { migrateManga.getSourceName(resultManga) }
val latestChapterAsync = async { migrateManga.getChapterInfo(searchResult as SearchResult.Result) }
sourceNameAsync.await() to latestChapterAsync.await()
}
binding.migrationMangaCardTo.attachManga(resultManga, sourceName, latestChapter)
jobs += binding.migrationMangaCardTo.root.clicks()
.onEach {
adapter.controller.router.pushController(
MangaController(
resultManga.id,
true,
),
)
}
.launchIn(adapter.controller.viewScope)
} else {
binding.migrationMangaCardTo.progress.isVisible = false
binding.migrationMangaCardTo.title.text = itemView.context
.getString(R.string.no_alternatives_found)
}
binding.migrationMenu.isVisible = true
binding.skipManga.isVisible = false
adapter.controller.sourceFinished()
}
.catch {
this@MigrationProcessHolder.logcat(throwable = it) { "Error updating result info" }
}
.launchIn(adapter.controller.viewScope)
}
}
private fun MigrationMangaCardBinding.resetManga() {
progress.isVisible = true
thumbnail.dispose()
thumbnail.setImageDrawable(null)
title.text = ""
mangaSourceLabel.text = ""
badges.unreadText.text = ""
badges.unreadText.isVisible = false
mangaLastChapterLabel.text = ""
}
private fun MigrationMangaCardBinding.attachManga(
manga: Manga,
sourceString: String,
chapterInfo: ChapterInfo,
) {
progress.isVisible = false
thumbnail.loadAutoPause(manga)
title.text = if (manga.title.isBlank()) {
itemView.context.getString(R.string.unknown)
} else {
manga.ogTitle
}
mangaSourceLabel.text = sourceString
// For rounded corners
badges.leftBadges.clipToOutline = true
badges.rightBadges.clipToOutline = true
badges.unreadText.isVisible = true
badges.unreadText.text = chapterInfo.chapterCount.toString()
if (chapterInfo.latestChapter != null && chapterInfo.latestChapter > 0f) {
mangaLastChapterLabel.text = itemView.context.getString(
R.string.latest_,
DecimalFormat("#.#").format(chapterInfo.latestChapter),
)
} else {
mangaLastChapterLabel.text = itemView.context.getString(
R.string.latest_,
root.context.getString(R.string.unknown),
)
}
}
private fun showPopupMenu(view: View) {
val item = adapter.getItem(bindingAdapterPosition) ?: return
// Create a PopupMenu, giving it the clicked view for an anchor
val popup = PopupMenu(view.context, view)
// Inflate our menu resource into the PopupMenu's Menu
popup.menuInflater.inflate(R.menu.migration_single, popup.menu)
val mangas = item.manga
popup.menu.findItem(R.id.action_search_manually).isVisible = true
// Hide download and show delete if the chapter is downloaded
if (mangas.searchResult.value != SearchResult.Searching) {
popup.menu.findItem(R.id.action_migrate_now).isVisible = true
popup.menu.findItem(R.id.action_copy_now).isVisible = true
}
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem ->
adapter.controller.onMenuItemClick(item.manga.manga.id, menuItem)
true
}
// Finally show the PopupMenu
popup.show()
}
}

View File

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
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.kanade.tachiyomi.R
class MigrationProcessItem(val manga: MigratingManga) :
AbstractFlexibleItem<MigrationProcessHolder>() {
override fun getLayoutRes(): Int {
return R.layout.migration_process_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MigrationProcessHolder {
return MigrationProcessHolder(view, adapter as MigrationProcessAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: MigrationProcessHolder,
position: Int,
payloads: MutableList<Any?>?,
) {
holder.bind(this)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is MigrationProcessItem) {
return manga.manga.id == other.manga.manga.id
}
return false
}
override fun hashCode(): Int {
return manga.manga.id.hashCode()
}
}