diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigratingManga.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigratingManga.kt index bb47ac799..accd04b2b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigratingManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigratingManga.kt @@ -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), + ) + } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt index a9c2a2889..47a1c83dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt @@ -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(bundle) { + FullComposeController(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(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?>( + 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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt index 79e19fcde..1396ee70a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt @@ -83,6 +83,8 @@ class MigrationListPresenter( private set val migratingItems = MutableStateFlow>(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) { 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() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt deleted file mode 100644 index 1a6a967bf..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt +++ /dev/null @@ -1,7 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.advanced.process - -import eu.davidea.flexibleadapter.FlexibleAdapter - -class MigrationProcessAdapter( - val controller: MigrationListController, -) : FlexibleAdapter(null, controller, true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt deleted file mode 100644 index 482c7a03b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt +++ /dev/null @@ -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() - - 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() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessItem.kt deleted file mode 100644 index 52a27633c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes(): Int { - return R.layout.migration_process_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MigrationProcessHolder { - return MigrationProcessHolder(view, adapter as MigrationProcessAdapter) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: MigrationProcessHolder, - position: Int, - payloads: MutableList?, - ) { - 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() - } -}