Use Voyager for migration

This commit is contained in:
Jobobby04 2022-11-28 19:41:04 -05:00
parent e7c2970561
commit d59d960c6a
18 changed files with 1155 additions and 965 deletions

View File

@ -0,0 +1,144 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForward
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.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.components.MigrationActionIcon
import eu.kanade.presentation.browse.components.MigrationItem
import eu.kanade.presentation.browse.components.MigrationItemResult
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga
import eu.kanade.tachiyomi.util.lang.withIOContext
@Composable
fun MigrationListScreen(
items: List<MigratingManga>,
migrationDone: Boolean,
unfinishedCount: Int,
getManga: suspend (MigratingManga.SearchResult.Result) -> Manga?,
getChapterInfo: suspend (MigratingManga.SearchResult.Result) -> MigratingManga.ChapterInfo,
getSourceName: (Manga) -> String,
onMigrationItemClick: (Manga) -> Unit,
openMigrationDialog: (Boolean) -> Unit,
skipManga: (Long) -> Unit,
searchManually: (MigratingManga) -> Unit,
migrateNow: (Long) -> Unit,
copyNow: (Long) -> Unit,
) {
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 = { onMigrationItemClick(migrationItem.manga) },
)
Icon(
Icons.Outlined.ArrowForward,
contentDescription = stringResource(R.string.migrating_to),
modifier = Modifier.weight(0.2f),
)
MigrationItemResult(
modifier = Modifier
.padding(top = 8.dp)
.weight(1f),
migrationItem = migrationItem,
result = result,
getManga = getManga,
getChapterInfo = getChapterInfo,
getSourceName = getSourceName,
onMigrationItemClick = onMigrationItemClick,
)
MigrationActionIcon(
modifier = Modifier
.weight(0.2f),
result = result,
skipManga = { skipManga(migrationItem.manga.id) },
searchManually = { searchManually(migrationItem) },
migrateNow = {
migrateNow(migrationItem.manga.id)
},
copyNow = {
copyNow(migrationItem.manga.id)
},
)
}
}
}
}
}

View File

@ -0,0 +1,88 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.Box
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga
import me.saket.cascade.CascadeDropdownMenu
@Composable
fun MigrationActionIcon(
modifier: Modifier,
result: MigratingManga.SearchResult,
skipManga: () -> Unit,
searchManually: () -> Unit,
migrateNow: () -> Unit,
copyNow: () -> Unit,
) {
var moreExpanded by remember { mutableStateOf(false) }
val closeMenu = { moreExpanded = false }
Box(modifier) {
if (result is MigratingManga.SearchResult.Searching) {
IconButton(onClick = skipManga) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_stop),
)
}
} else if (result is MigratingManga.SearchResult.Result || result is MigratingManga.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),
) {
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringResource(R.string.action_search_manually)) },
onClick = {
searchManually()
closeMenu()
},
)
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringResource(R.string.action_skip_manga)) },
onClick = {
skipManga()
closeMenu()
},
)
if (result is MigratingManga.SearchResult.Result) {
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringResource(R.string.action_migrate_now)) },
onClick = {
migrateNow()
closeMenu()
},
)
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringResource(R.string.action_copy_now)) },
onClick = {
copyNow()
closeMenu()
},
)
}
}
}
}
}

View File

@ -0,0 +1,31 @@
package eu.kanade.presentation.browse.components
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
@Composable
fun MigrationExitDialog(
onDismissRequest: () -> Unit,
exitMigration: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = exitMigration) {
Text(text = stringResource(R.string.action_stop))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
}
},
title = {
Text(text = stringResource(R.string.stop_migrating))
},
)
}

View File

@ -0,0 +1,118 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.MangaCover
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga
import eu.kanade.tachiyomi.util.lang.withIOContext
@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,
)
}
}

View File

@ -0,0 +1,84 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga
import eu.kanade.tachiyomi.util.lang.withIOContext
@Composable
fun MigrationItemResult(
modifier: Modifier,
migrationItem: MigratingManga,
result: MigratingManga.SearchResult,
getManga: suspend (MigratingManga.SearchResult.Result) -> Manga?,
getChapterInfo: suspend (MigratingManga.SearchResult.Result) -> MigratingManga.ChapterInfo,
getSourceName: (Manga) -> String,
onMigrationItemClick: (Manga) -> Unit,
) {
Box(modifier) {
when (result) {
MigratingManga.SearchResult.Searching -> Box(
modifier = Modifier
.widthIn(max = 150.dp)
.fillMaxWidth()
.aspectRatio(MangaCover.Book.ratio),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
MigratingManga.SearchResult.NotFound -> Image(
painter = rememberResourceBitmapPainter(id = R.drawable.cover_error),
contentDescription = null,
modifier = Modifier
.matchParentSize()
.clip(RoundedCornerShape(4.dp)),
contentScale = ContentScale.Crop,
)
is MigratingManga.SearchResult.Result -> {
val item by produceState<Triple<Manga, MigratingManga.ChapterInfo, String>?>(
initialValue = null,
migrationItem,
result,
) {
value = withIOContext {
val manga = getManga(result) ?: return@withIOContext null
Triple(
manga,
getChapterInfo(result),
getSourceName(manga),
)
}
}
if (item != null) {
val (manga, chapterInfo, source) = item!!
MigrationItem(
modifier = Modifier,
manga = manga,
sourcesString = source,
chapterInfo = chapterInfo,
onClick = {
onMigrationItemClick(manga)
},
)
}
}
}
}
}

View File

@ -0,0 +1,51 @@
package eu.kanade.presentation.browse.components
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
@Composable
fun MigrationMangaDialog(
onDismissRequest: () -> Unit,
copy: Boolean,
mangaSet: Int,
mangaSkipped: Int,
copyManga: () -> Unit,
migrateManga: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
onClick = {
if (copy) {
copyManga()
} else {
migrateManga()
}
},
) {
Text(text = stringResource(if (copy) R.string.copy else R.string.migrate))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
}
},
text = {
Text(
text = pluralStringResource(
if (copy) R.plurals.copy_manga else R.plurals.migrate_manga,
count = mangaSet,
mangaSet,
(if (mangaSkipped > 0) " " + stringResource(R.string.skipping_, mangaSkipped) else ""),
),
)
},
)
}

View File

@ -1,44 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration
import android.app.Dialog
import android.os.Bundle
import com.bluelinelabs.conductor.Controller
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController
class MigrationMangaDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
where T : Controller {
var copy = false
var mangaSet = 0
var mangaSkipped = 0
constructor(target: T, copy: Boolean, mangaSet: Int, mangaSkipped: Int) : this() {
targetController = target
this.copy = copy
this.mangaSet = mangaSet
this.mangaSkipped = mangaSkipped
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val confirmRes = if (copy) R.plurals.copy_manga else R.plurals.migrate_manga
val confirmString = applicationContext?.resources?.getQuantityString(
confirmRes,
mangaSet,
mangaSet,
(if (mangaSkipped > 0) " " + applicationContext?.getString(R.string.skipping_, mangaSkipped) else ""),
).orEmpty()
return MaterialAlertDialogBuilder(activity!!)
.setMessage(confirmString)
.setPositiveButton(if (copy) R.string.copy else R.string.migrate) { _, _ ->
if (copy) {
(targetController as? MigrationListController)?.copyMangas()
} else {
(targetController as? MigrationListController)?.migrateMangas()
}
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.app.Activity
import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import android.view.LayoutInflater
@ -21,13 +21,13 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import uy.kohesive.injekt.injectLazy
class MigrationBottomSheetDialog(private val activity: Activity, private val listener: StartMigrationListener) : BaseBottomSheetDialog(activity) {
class MigrationBottomSheetDialog(private val baseContext: Context, private val listener: StartMigrationListener) : BaseBottomSheetDialog(baseContext) {
private val preferences: UnsortedPreferences by injectLazy()
lateinit var binding: MigrationBottomSheetBinding
override fun createView(inflater: LayoutInflater): View {
binding = MigrationBottomSheetBinding.inflate(activity.layoutInflater)
binding = MigrationBottomSheetBinding.inflate(LayoutInflater.from(baseContext))
return binding.root
}

View File

@ -1,16 +1,15 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.os.Bundle
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.SourceManager
import uy.kohesive.injekt.injectLazy
class MigrationSourceAdapter(
controllerPre: PreMigrationController,
listener: FlexibleAdapter.OnItemClickListener,
) : FlexibleAdapter<MigrationSourceItem>(
null,
controllerPre,
listener,
true,
) {
val sourceManager: SourceManager by injectLazy()
@ -18,32 +17,4 @@ class MigrationSourceAdapter(
// SY _->
val sourcePreferences: SourcePreferences by injectLazy()
// SY <--
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(
SELECTED_SOURCES_KEY,
ArrayList(
currentItems.map {
it.asParcelable()
},
),
)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
val selectedSources = savedInstanceState
.getParcelableArrayList<MigrationSourceItem.MigrationSource>(SELECTED_SOURCES_KEY)
if (selectedSources != null) {
updateDataSet(selectedSources.map { MigrationSourceItem.fromParcelable(sourceManager, it) })
}
super.onRestoreInstanceState(savedInstanceState)
}
companion object {
private const val SELECTED_SOURCES_KEY = "selected_sources"
}
}

View File

@ -1,65 +1,16 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Deselect
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.navigator.Navigator
import com.bluelinelabs.conductor.Router
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.OverflowMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
import eu.kanade.tachiyomi.util.lang.launchIO
import uy.kohesive.injekt.injectLazy
import kotlin.math.roundToInt
class PreMigrationController(bundle: Bundle? = null) :
BasicFullComposeController(bundle),
FlexibleAdapter.OnItemClickListener,
StartMigrationListener {
class PreMigrationController(bundle: Bundle? = null) : BasicFullComposeController(bundle) {
constructor(mangaIds: List<Long>) : this(
bundleOf(
@ -67,275 +18,11 @@ class PreMigrationController(bundle: Bundle? = null) :
),
)
private val sourceManager: SourceManager by injectLazy()
private val prefs: UnsortedPreferences by injectLazy()
private val sourcePreferences: SourcePreferences by injectLazy()
private var adapter: MigrationSourceAdapter? = null
private val config: LongArray = args.getLongArray(MANGA_IDS_EXTRA) ?: LongArray(0)
private lateinit var dialog: MigrationBottomSheetDialog
private lateinit var controllerBinding: PreMigrationListBinding
var items by mutableStateOf(emptyList<MigrationSourceItem>())
override fun onViewCreated(view: View) {
super.onViewCreated(view)
dialog = MigrationBottomSheetDialog(activity!!, this)
viewScope.launchIO {
items = getEnabledSources()
}
}
@Composable
override fun ComposeContent() {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
var fabExpanded by remember { mutableStateOf(true) }
val nestedScrollConnection = remember {
// All this lines just for fab state :/
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
fabExpanded = available.y >= 0
return scrollBehavior.nestedScrollConnection.onPreScroll(available, source)
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
return scrollBehavior.nestedScrollConnection.onPreFling(available)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available)
}
}
}
Scaffold(
topBar = {
AppBar(
title = stringResource(R.string.select_sources),
navigateUp = router::popCurrentController,
scrollBehavior = scrollBehavior,
actions = {
IconButton(onClick = { massSelect(false) }) {
Icon(
imageVector = Icons.Outlined.Deselect,
contentDescription = stringResource(R.string.select_none),
)
}
IconButton(onClick = { massSelect(true) }) {
Icon(
imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all),
)
}
OverflowMenu { closeMenu ->
DropdownMenuItem(
text = { Text(stringResource(R.string.match_enabled_sources)) },
onClick = {
matchSelection(true)
closeMenu()
},
)
DropdownMenuItem(
text = { Text(stringResource(R.string.match_pinned_sources)) },
onClick = {
matchSelection(false)
closeMenu()
},
)
}
},
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(R.string.action_migrate)) },
icon = {
Icon(
imageVector = Icons.Outlined.ArrowForward,
contentDescription = stringResource(R.string.action_migrate),
)
},
onClick = {
if (!dialog.isShowing) {
dialog.show()
}
},
expanded = fabExpanded,
modifier = Modifier.navigationBarsPadding(),
)
},
) { contentPadding ->
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() }
val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() }
val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() }
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
AndroidView(
factory = { context ->
controllerBinding = PreMigrationListBinding.inflate(LayoutInflater.from(context))
adapter = MigrationSourceAdapter(this@PreMigrationController)
controllerBinding.recycler.adapter = adapter
adapter?.isHandleDragEnabled = true
adapter?.fastScroller = controllerBinding.fastScroller
controllerBinding.recycler.layoutManager = LinearLayoutManager(context)
ViewCompat.setNestedScrollingEnabled(controllerBinding.root, true)
controllerBinding.root
},
update = {
controllerBinding.recycler
.updatePadding(
left = left,
top = top,
right = right,
bottom = bottom,
)
controllerBinding.fastScroller
.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = left
topMargin = top
rightMargin = right
bottomMargin = bottom
}
adapter?.updateDataSet(items)
},
)
}
}
}
override fun startMigration(extraParam: String?) {
val listOfSources = adapter?.currentItems
?.filterIsInstance<MigrationSourceItem>()
?.filter {
it.sourceEnabled
}
?.joinToString("/") { it.source.id.toString() }
.orEmpty()
prefs.migrationSources().set(listOfSources)
router.replaceTopController(
MigrationListController(
MigrationProcedureConfig(
config.toList(),
extraSearchParams = extraParam,
),
).withFadeTransaction().tag(MigrationListController.TAG),
)
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
adapter?.onSaveInstanceState(outState)
}
// TODO Still incorrect, why is this called before onViewCreated?
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
adapter?.onRestoreInstanceState(savedInstanceState)
}
override fun onItemClick(view: View, position: Int): Boolean {
val adapter = adapter ?: return false
adapter.getItem(position)?.let {
it.sourceEnabled = !it.sourceEnabled
}
adapter.notifyItemChanged(position)
return false
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<MigrationSourceItem> {
val languages = sourcePreferences.enabledLanguages().get()
val sourcesSaved = prefs.migrationSources().get().split("/")
.mapNotNull { it.toLongOrNull() }
val disabledSources = sourcePreferences.disabledSources().get()
.mapNotNull { it.toLongOrNull() }
val sources = sourceManager.getVisibleCatalogueSources()
.asSequence()
.filterIsInstance<HttpSource>()
.filter { it.lang in languages }
.sortedBy { "(${it.lang}) ${it.name}" }
.map {
MigrationSourceItem(
it,
isEnabled(
sourcesSaved,
disabledSources,
it.id,
),
)
}
.toList()
return sources
.filter { it.sourceEnabled }
.sortedBy { sourcesSaved.indexOf(it.source.id) }
.plus(
sources.filterNot { it.sourceEnabled },
)
}
fun isEnabled(
sourcesSaved: List<Long>,
disabledSources: List<Long>,
id: Long,
): Boolean {
return if (sourcesSaved.isEmpty()) {
id !in disabledSources
} else {
id in sourcesSaved
}
}
private fun massSelect(selectAll: Boolean) {
val adapter = adapter ?: return
adapter.currentItems.forEach {
it.sourceEnabled = selectAll
}
adapter.notifyDataSetChanged()
}
private fun matchSelection(matchEnabled: Boolean) {
val adapter = adapter ?: return
val enabledSources = if (matchEnabled) {
sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() }
} else {
sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() }
}
val items = adapter.currentItems.toList()
items.forEach {
it.sourceEnabled = if (matchEnabled) {
it.source.id !in enabledSources
} else {
it.source.id in enabledSources
}
}
val sortedItems = items.sortedBy { it.source.name }.sortedBy { !it.sourceEnabled }
adapter.updateDataSet(sortedItems)
Navigator(screen = PreMigrationScreen(config.toList()))
}
companion object {
@ -349,7 +36,7 @@ class PreMigrationController(bundle: Bundle? = null) :
)
} else {
PreMigrationController(mangaIds)
}.withFadeTransaction().tag(if (skipPre) MigrationListController.TAG else null),
}.withFadeTransaction().tag(MigrationListController.TAG),
)
}
}

View File

@ -0,0 +1,217 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Deselect
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.OverflowMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
import kotlin.math.roundToInt
class PreMigrationScreen(val mangaIds: List<Long>) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { PreMigrationScreenModel() }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
var fabExpanded by remember { mutableStateOf(true) }
val items by screenModel.state.collectAsState()
val context = LocalContext.current
DisposableEffect(screenModel) {
screenModel.dialog = MigrationBottomSheetDialog(context, screenModel.listener)
onDispose {}
}
LaunchedEffect(screenModel) {
screenModel.startMigration.collect { extraParam ->
navigator replace MigrationListScreen(MigrationProcedureConfig(mangaIds, extraParam))
}
}
val nestedScrollConnection = remember {
// All this lines just for fab state :/
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
fabExpanded = available.y >= 0
return scrollBehavior.nestedScrollConnection.onPreScroll(available, source)
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
return scrollBehavior.nestedScrollConnection.onPreFling(available)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available)
}
}
}
Scaffold(
topBar = {
AppBar(
title = stringResource(R.string.select_sources),
navigateUp = {
when {
navigator.canPop -> navigator.pop()
else -> router.popCurrentController()
}
},
scrollBehavior = scrollBehavior,
actions = {
IconButton(onClick = { screenModel.massSelect(false) }) {
Icon(
imageVector = Icons.Outlined.Deselect,
contentDescription = stringResource(R.string.select_none),
)
}
IconButton(onClick = { screenModel.massSelect(true) }) {
Icon(
imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all),
)
}
OverflowMenu { closeMenu ->
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringResource(R.string.match_enabled_sources)) },
onClick = {
screenModel.matchSelection(true)
closeMenu()
},
)
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringResource(R.string.match_pinned_sources)) },
onClick = {
screenModel.matchSelection(false)
closeMenu()
},
)
}
},
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(R.string.action_migrate)) },
icon = {
Icon(
imageVector = Icons.Outlined.ArrowForward,
contentDescription = stringResource(R.string.action_migrate),
)
},
onClick = {
if (!screenModel.dialog.isShowing) {
screenModel.dialog.show()
}
},
expanded = fabExpanded,
modifier = Modifier.navigationBarsPadding(),
)
},
) { contentPadding ->
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() }
val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() }
val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() }
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
AndroidView(
factory = { context ->
screenModel.controllerBinding = PreMigrationListBinding.inflate(LayoutInflater.from(context))
screenModel.adapter = MigrationSourceAdapter(screenModel.clickListener)
screenModel.controllerBinding.recycler.adapter = screenModel.adapter
screenModel.adapter?.isHandleDragEnabled = true
screenModel.adapter?.fastScroller = screenModel.controllerBinding.fastScroller
screenModel.controllerBinding.recycler.layoutManager = LinearLayoutManager(context)
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
screenModel.controllerBinding.root
},
update = {
screenModel.controllerBinding.recycler
.updatePadding(
left = left,
top = top,
right = right,
bottom = bottom,
)
screenModel.controllerBinding.fastScroller
.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = left
topMargin = top
rightMargin = right
bottomMargin = bottom
}
screenModel.adapter?.updateDataSet(items)
},
)
}
}
}
companion object {
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, mangaIds: List<Long>) {
navigator.push(
if (skipPre) {
MigrationListScreen(
MigrationProcedureConfig(mangaIds, null),
)
} else {
PreMigrationScreen(mangaIds)
},
)
}
}
}

View File

@ -0,0 +1,143 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class PreMigrationScreenModel(
private val sourceManager: SourceManager = Injekt.get(),
private val prefs: UnsortedPreferences = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
) : ScreenModel {
private val _state = MutableStateFlow(emptyList<MigrationSourceItem>())
val state = _state.asStateFlow()
lateinit var controllerBinding: PreMigrationListBinding
var adapter: MigrationSourceAdapter? = null
val startMigration = MutableSharedFlow<String?>()
val listener = object : StartMigrationListener {
override fun startMigration(extraParam: String?) {
val listOfSources = adapter?.currentItems
?.filterIsInstance<MigrationSourceItem>()
?.filter {
it.sourceEnabled
}
?.joinToString("/") { it.source.id.toString() }
.orEmpty()
prefs.migrationSources().set(listOfSources)
coroutineScope.launch {
startMigration.emit(extraParam)
}
}
}
val clickListener = FlexibleAdapter.OnItemClickListener { _, position ->
val adapter = adapter ?: return@OnItemClickListener false
adapter.getItem(position)?.let {
it.sourceEnabled = !it.sourceEnabled
}
adapter.notifyItemChanged(position)
false
}
lateinit var dialog: MigrationBottomSheetDialog
init {
coroutineScope.launchIO {
val enabledSources = getEnabledSources()
_state.update { enabledSources }
}
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<MigrationSourceItem> {
val languages = sourcePreferences.enabledLanguages().get()
val sourcesSaved = prefs.migrationSources().get().split("/")
.mapNotNull { it.toLongOrNull() }
val disabledSources = sourcePreferences.disabledSources().get()
.mapNotNull { it.toLongOrNull() }
val sources = sourceManager.getVisibleCatalogueSources()
.asSequence()
.filterIsInstance<HttpSource>()
.filter { it.lang in languages }
.sortedBy { "(${it.lang}) ${it.name}" }
.map {
MigrationSourceItem(
it,
isEnabled(
sourcesSaved,
disabledSources,
it.id,
),
)
}
.toList()
return sources
.filter { it.sourceEnabled }
.sortedBy { sourcesSaved.indexOf(it.source.id) }
.plus(
sources.filterNot { it.sourceEnabled },
)
}
fun isEnabled(
sourcesSaved: List<Long>,
disabledSources: List<Long>,
id: Long,
): Boolean {
return if (sourcesSaved.isEmpty()) {
id !in disabledSources
} else {
id in sourcesSaved
}
}
fun massSelect(selectAll: Boolean) {
val adapter = adapter ?: return
adapter.currentItems.forEach {
it.sourceEnabled = selectAll
}
adapter.notifyDataSetChanged()
}
fun matchSelection(matchEnabled: Boolean) {
val adapter = adapter ?: return
val enabledSources = if (matchEnabled) {
sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() }
} else {
sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() }
}
val items = adapter.currentItems.toList()
items.forEach {
it.sourceEnabled = if (matchEnabled) {
it.source.id !in enabledSources
} else {
it.source.id in enabledSources
}
}
val sortedItems = items.sortedBy { it.source.name }.sortedBy { !it.sourceEnabled }
adapter.updateDataSet(sortedItems)
}
}

View File

@ -1,84 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.os.Bundle
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
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.source.Source
import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler
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
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
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.toast
import me.saket.cascade.CascadeDropdownMenu
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.util.system.getSerializableCompat
class MigrationListController(bundle: Bundle? = null) :
FullComposeController<MigrationListPresenter>(bundle) {
BasicFullComposeController(bundle) {
constructor(config: MigrationProcedureConfig) : this(
bundleOf(
@ -86,414 +16,11 @@ class MigrationListController(bundle: Bundle? = null) :
),
)
val config = args.getParcelableCompat<MigrationProcedureConfig>(CONFIG_EXTRA)
private var selectedMangaId: Long? = null
private var manualMigrations = 0
override fun createPresenter(): MigrationListPresenter {
return MigrationListPresenter(config!!)
}
val config = args.getSerializableCompat<MigrationProcedureConfig>(CONFIG_EXTRA)!!
@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,
),
)
},
)
Icon(
Icons.Outlined.ArrowForward,
contentDescription = stringResource(R.string.migrating_to),
modifier = Modifier.weight(0.2f),
)
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++
},
)
}
}
}
}
}
@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() {
val res = resources
if (res != null) {
activity?.toast(
res.getQuantityString(
R.plurals.manga_migrated,
manualMigrations,
manualMigrations,
),
)
}
if (!presenter.hideNotFound) {
router.popCurrentController()
}
}
fun useMangaForMigration(manga: Manga, source: Source) {
presenter.useMangaForMigration(manga, source, selectedMangaId ?: return)
}
fun migrateMangas() {
presenter.migrateMangas()
}
fun copyMangas() {
presenter.copyMangas()
}
fun migrateManga(mangaId: Long, copy: Boolean) {
presenter.migrateManga(mangaId, copy)
}
fun removeManga(mangaId: Long) {
presenter.removeManga(mangaId)
}
fun sourceFinished() {
if (presenter.migratingItems.value.isEmpty()) noMigration()
}
fun navigateOut(manga: Manga?) {
if (manga != null) {
val newStack = router.backstack.filter {
it.controller !is MangaController &&
it.controller !is MigrationListController &&
it.controller !is PreMigrationController
} + MangaController(manga.id).withFadeTransaction()
router.setBackstack(newStack, OneWayFadeChangeHandler())
return
}
router.popCurrentController()
}
override fun handleBack(): Boolean {
activity?.let {
MaterialAlertDialogBuilder(it)
.setTitle(R.string.stop_migrating)
.setPositiveButton(R.string.action_stop) { _, _ ->
router.popCurrentController()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
return true
}
private fun openMigrationDialog(copy: Boolean) {
val totalManga = presenter.migratingItems.value.size
val mangaSkipped = presenter.mangasSkipped()
MigrationMangaDialog(
this,
copy,
totalManga,
mangaSkipped,
).showDialog(router)
Navigator(screen = MigrationListScreen(config))
}
companion object {

View File

@ -0,0 +1,177 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrationListScreen
import eu.kanade.presentation.browse.components.MigrationExitDialog
import eu.kanade.presentation.browse.components.MigrationMangaDialog
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast
class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { MigrationListScreenModel(config) }
val items by screenModel.migratingItems.collectAsState()
val migrationDone by screenModel.migrationDone.collectAsState()
val unfinishedCount by screenModel.unfinishedCount.collectAsState()
val dialog by screenModel.dialog.collectAsState()
val navigator = LocalNavigator.currentOrThrow
val router = LocalRouter.currentOrThrow
val context = LocalContext.current
LaunchedEffect(items) {
if (items.isEmpty()) {
val manualMigrations = screenModel.manualMigrations.value
context.toast(
context.resources.getQuantityString(
R.plurals.manga_migrated,
manualMigrations,
manualMigrations,
),
)
if (!screenModel.hideNotFound) {
if (navigator.canPop) {
navigator.pop()
} else {
router.popCurrentController()
}
}
}
}
LaunchedEffect(screenModel) {
screenModel.navigateOut.collect {
if (navigator.canPop) {
if (items.size == 1) {
val hasDetails = navigator.items.any { it is MangaScreen }
if (hasDetails) {
val manga = (items.firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.let {
screenModel.getManga(it.id)
}
withUIContext {
if (manga != null) {
val newStack = navigator.items.filter {
it !is MangaScreen &&
it !is MigrationListScreen &&
it !is PreMigrationScreen
} + MangaScreen(manga.id)
navigator replaceAll newStack.first()
navigator.push(newStack.drop(1))
} else {
navigator.pop()
}
}
}
} else {
withUIContext {
navigator.pop()
}
}
} else {
if (items.size == 1) {
val hasDetails = router.backstack.any { it.controller is MangaController }
if (hasDetails) {
val manga = (items.firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.let {
screenModel.getManga(it.id)
}
withUIContext {
if (manga != null) {
val newStack = router.backstack.filter {
it.controller !is MangaController &&
it.controller !is MigrationListController &&
it.controller !is PreMigrationController
} + MangaController(manga.id).withFadeTransaction()
router.setBackstack(newStack, OneWayFadeChangeHandler())
} else {
router.popCurrentController()
}
}
}
} else {
withUIContext {
router.popCurrentController()
}
}
}
}
}
MigrationListScreen(
items = items,
migrationDone = migrationDone,
unfinishedCount = unfinishedCount,
getManga = screenModel::getManga,
getChapterInfo = screenModel::getChapterInfo,
getSourceName = screenModel::getSourceName,
onMigrationItemClick = {
navigator.push(MangaScreen(it.id, true))
},
openMigrationDialog = screenModel::openMigrateDialog,
skipManga = { screenModel.removeManga(it) },
searchManually = { migrationItem ->
val sources = screenModel.getMigrationSources()
val validSources = if (sources.size == 1) {
sources
} else {
sources.filter { it.id != migrationItem.manga.source }
}
val searchController = SearchController(migrationItem.manga, validSources)
searchController.useMangaForMigration = { manga, source ->
screenModel.useMangaForMigration(context, manga, source, migrationItem.manga.id)
}
router.pushController(searchController)
},
migrateNow = { screenModel.migrateManga(it, false) },
copyNow = { screenModel.migrateManga(it, true) },
)
val onDismissRequest = { screenModel.dialog.value = null }
when (val dialog = dialog) {
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
MigrationMangaDialog(
onDismissRequest = onDismissRequest,
copy = dialog.copy,
mangaSet = dialog.mangaSet,
mangaSkipped = dialog.mangaSkipped,
copyManga = screenModel::copyMangas,
migrateManga = screenModel::migrateMangas,
)
}
MigrationListScreenModel.Dialog.MigrationExitDialog -> {
MigrationExitDialog(
onDismissRequest = onDismissRequest,
exitMigration = {
if (navigator.canPop) {
navigator.pop()
} else {
router.popCurrentController()
}
},
)
}
null -> Unit
}
BackHandler(true) {
screenModel.dialog.value = MigrationListScreenModel.Dialog.MigrationExitDialog
}
}
}

View File

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.os.Bundle
import android.content.Context
import android.widget.Toast
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories
@ -30,10 +32,8 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
@ -42,10 +42,11 @@ import exh.eh.EHentaiThrottleManager
import exh.smartsearch.SmartSearchEngine
import exh.source.MERGED_SOURCE_ID
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Semaphore
@ -55,7 +56,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.atomic.AtomicInteger
class MigrationListPresenter(
class MigrationListScreenModel(
private val config: MigrationProcedureConfig,
private val preferences: UnsortedPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
@ -74,56 +75,56 @@ class MigrationListPresenter(
private val getTracks: GetTracks = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
private val deleteTrack: DeleteTrack = Injekt.get(),
) : BasePresenter<MigrationListController>() {
) : ScreenModel {
private val smartSearchEngine = SmartSearchEngine(config.extraSearchParams)
private val throttleManager = EHentaiThrottleManager()
var migrationsJob: Job? = null
private set
val migratingItems = MutableStateFlow<List<MigratingManga>>(emptyList())
val migrationDone = MutableStateFlow(false)
val unfinishedCount = MutableStateFlow(0)
val manualMigrations = MutableStateFlow(0)
val hideNotFound = preferences.hideNotFoundMigration().get()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
val navigateOut = MutableSharedFlow<Unit>()
if (migrationsJob?.isActive != true) {
migrationsJob = presenterScope.launchIO {
runMigrations(
config.mangaIds
.map {
async {
val manga = getManga.await(it) ?: return@async null
MigratingManga(
manga = manga,
chapterInfo = getChapterInfo(it),
sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
if (manga.source == MERGED_SOURCE_ID) {
getMergedReferencesById.await(manga.id)
.map { sourceManager.getOrStub(it.mangaSourceId) }
} else {
null
},
),
parentContext = presenterScope.coroutineContext,
)
}
val dialog = MutableStateFlow<Dialog?>(null)
init {
coroutineScope.launchIO {
runMigrations(
config.mangaIds
.map {
async {
val manga = getManga.await(it) ?: return@async null
MigratingManga(
manga = manga,
chapterInfo = getChapterInfo(it),
sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
if (manga.source == MERGED_SOURCE_ID) {
getMergedReferencesById.await(manga.id)
.map { sourceManager.getOrStub(it.mangaSourceId) }
} else {
null
},
),
parentContext = coroutineScope.coroutineContext,
)
}
.awaitAll()
.filterNotNull()
.also {
migratingItems.value = it
},
)
}
}
.awaitAll()
.filterNotNull()
.also {
migratingItems.value = it
},
)
}
}
suspend fun getManga(result: SearchResult.Result) = getManga.await(result.id)
suspend fun getManga(result: SearchResult.Result) = getManga(result.id)
suspend fun getManga(id: Long) = getManga.await(id)
suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id)
suspend fun getChapterInfo(id: Long) = getChapterByMangaId.await(id).let { chapters ->
MigratingManga.ChapterInfo(
@ -146,7 +147,7 @@ class MigrationListPresenter(
val sources = getMigrationSources()
for (manga in mangas) {
if (migrationsJob?.isCancelled == true) {
if (!currentCoroutineContext().isActive) {
break
}
// in case it was removed
@ -224,7 +225,7 @@ class MigrationListPresenter(
source.getChapterList(localManga.toSManga())
}
} catch (e: Exception) {
this@MigrationListPresenter.logcat(LogPriority.ERROR, e)
this@MigrationListScreenModel.logcat(LogPriority.ERROR, e)
emptyList()
}
syncChaptersWithSource.await(chapters, localManga, source)
@ -274,14 +275,13 @@ class MigrationListPresenter(
}
}
private suspend fun sourceFinished() {
private 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 } &&
@ -383,11 +383,11 @@ class MigrationListPresenter(
updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate))
}
fun useMangaForMigration(manga: Manga, source: Source, selectedMangaId: Long) {
fun useMangaForMigration(context: Context, manga: Manga, source: Source, selectedMangaId: Long) {
val migratingManga = migratingItems.value.find { it.manga.id == selectedMangaId }
?: return
migratingManga.searchResult.value = SearchResult.Searching
presenterScope.launchIO {
coroutineScope.launchIO {
val result = migratingManga.migrationScope.async {
val localManga = networkToLocalManga.await(manga)
try {
@ -413,14 +413,14 @@ class MigrationListPresenter(
} else {
migratingManga.searchResult.value = SearchResult.NotFound
withUIContext {
view?.activity?.toast(R.string.no_chapters_found_for_migration, Toast.LENGTH_LONG)
context.toast(R.string.no_chapters_found_for_migration, Toast.LENGTH_LONG)
}
}
}
}
fun migrateMangas() {
presenterScope.launchIO {
coroutineScope.launchIO {
migratingItems.value.forEach { manga ->
val searchResult = manga.searchResult.value
if (searchResult is SearchResult.Result) {
@ -438,7 +438,7 @@ class MigrationListPresenter(
}
fun copyMangas() {
presenterScope.launchIO {
coroutineScope.launchIO {
migratingItems.value.forEach { manga ->
val searchResult = manga.searchResult.value
if (searchResult is SearchResult.Result) {
@ -455,26 +455,12 @@ class MigrationListPresenter(
}
private suspend fun navigateOut() {
val view = view ?: return
if (migratingItems.value.size == 1) {
val hasDetails = view.router.backstack.any { it.controller is MangaController }
if (hasDetails) {
val manga = (migratingItems.value.firstOrNull()?.searchResult?.value as? SearchResult.Result)?.let {
getManga.await(it.id)
}
withUIContext {
view.navigateOut(manga)
}
return
}
}
withUIContext {
view.navigateOut(null)
}
navigateOut.emit(Unit)
}
fun migrateManga(mangaId: Long, copy: Boolean) {
presenterScope.launchIO {
manualMigrations.value++
coroutineScope.launchIO {
val manga = migratingItems.value.find { it.manga.id == mangaId }
?: return@launchIO
@ -491,7 +477,7 @@ class MigrationListPresenter(
}
fun removeManga(mangaId: Long) {
presenterScope.launchIO {
coroutineScope.launchIO {
val item = migratingItems.value.find { it.manga.id == mangaId }
?: return@launchIO
if (migratingItems.value.size == 1) {
@ -517,11 +503,25 @@ class MigrationListPresenter(
}
}
override fun onDestroy() {
super.onDestroy()
migrationsJob?.cancel()
override fun onDispose() {
super.onDispose()
migratingItems.value.forEach {
it.migrationScope.cancel()
}
}
fun openMigrateDialog(
copy: Boolean,
) {
dialog.value = Dialog.MigrateMangaDialog(
copy,
migratingItems.value.size,
mangasSkipped(),
)
}
sealed class Dialog {
data class MigrateMangaDialog(val copy: Boolean, val mangaSet: Int, val mangaSkipped: Int) : Dialog()
object MigrationExitDialog : Dialog()
}
}

View File

@ -1,10 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.io.Serializable
@Parcelize
data class MigrationProcedureConfig(
var mangaIds: List<Long>,
val extraSearchParams: String?,
) : Parcelable
) : Serializable

View File

@ -5,9 +5,9 @@ import androidx.core.os.bundleOf
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
import kotlinx.coroutines.runBlocking
@ -24,24 +24,23 @@ class SearchController(
SOURCES to sources?.map { it.id }?.toLongArray(),
),
) {
constructor(targetController: MigrationListController?, mangaId: Long, sources: LongArray) :
constructor(mangaId: Long, sources: LongArray) :
this(
runBlocking {
Injekt.get<GetManga>()
.await(mangaId)
},
sources.map { Injekt.get<SourceManager>().getOrStub(it) }.filterIsInstance<CatalogueSource>(),
) {
this.targetController = targetController
}
)
@Suppress("unused")
constructor(bundle: Bundle) : this(
null,
bundle.getLong(OLD_MANGA),
bundle.getLongArray(SOURCES) ?: LongArray(0),
)
var useMangaForMigration: ((Manga, Source) -> Unit)? = null
/**
* Called when controller is initialized.
*/
@ -58,10 +57,9 @@ class SearchController(
}
override fun onMangaClick(manga: Manga) {
val migrationListController = targetController as MigrationListController
val sourceManager = Injekt.get<SourceManager>()
val source = sourceManager.get(manga.source) ?: return
migrationListController.useMangaForMigration(manga, source)
useMangaForMigration?.let { it(manga, source) }
router.popCurrentController()
}
@ -73,7 +71,7 @@ class SearchController(
override fun onTitleClick(source: CatalogueSource) {
presenter.sourcePreferences.lastUsedSource().set(source.id)
router.pushController(SourceSearchController(targetController as? MigrationListController ?: return, manga!!, source, presenter.query))
router.pushController(SourceSearchController(manga!!, source, presenter.query).also { it.useMangaForMigration = useMangaForMigration })
}
companion object {

View File

@ -7,9 +7,9 @@ import androidx.core.os.bundleOf
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.SourceSearchScreen
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import uy.kohesive.injekt.Injekt
@ -19,15 +19,15 @@ class SourceSearchController(
bundle: Bundle,
) : BrowseSourceController(bundle) {
constructor(targetController: MigrationListController, manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this(
constructor(manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this(
bundleOf(
SOURCE_ID_KEY to source.id,
MANGA_KEY to manga,
SEARCH_QUERY_KEY to searchQuery,
),
) {
this.targetController = targetController
}
)
var useMangaForMigration: ((Manga, Source) -> Unit)? = null
@Composable
override fun ComposeContent() {
@ -37,11 +37,11 @@ class SourceSearchController(
onFabClick = { filterSheet?.show() },
// SY -->
onMangaClick = { manga ->
val migrationListController = targetController as? MigrationListController ?: return@SourceSearchScreen
val sourceManager = Injekt.get<SourceManager>()
val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
migrationListController.useMangaForMigration(manga, source)
router.popToTag(MigrationListController.TAG)
useMangaForMigration?.let { it(manga, source) }
router.popCurrentController()
router.popCurrentController()
},
// SY <--
onWebViewClick = f@{