Use Voyager for migration
This commit is contained in:
parent
e7c2970561
commit
d59d960c6a
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
},
|
||||
)
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 ""),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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@{
|
||||
|
Loading…
x
Reference in New Issue
Block a user