Convert mass migration to compose
This commit is contained in:
parent
aaddb4bf00
commit
e696b95330
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user