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
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
||||||
|
|
||||||
import android.app.Activity
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -21,13 +21,13 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
||||||
import uy.kohesive.injekt.injectLazy
|
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()
|
private val preferences: UnsortedPreferences by injectLazy()
|
||||||
|
|
||||||
lateinit var binding: MigrationBottomSheetBinding
|
lateinit var binding: MigrationBottomSheetBinding
|
||||||
|
|
||||||
override fun createView(inflater: LayoutInflater): View {
|
override fun createView(inflater: LayoutInflater): View {
|
||||||
binding = MigrationBottomSheetBinding.inflate(activity.layoutInflater)
|
binding = MigrationBottomSheetBinding.inflate(LayoutInflater.from(baseContext))
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class MigrationSourceAdapter(
|
class MigrationSourceAdapter(
|
||||||
controllerPre: PreMigrationController,
|
listener: FlexibleAdapter.OnItemClickListener,
|
||||||
) : FlexibleAdapter<MigrationSourceItem>(
|
) : FlexibleAdapter<MigrationSourceItem>(
|
||||||
null,
|
null,
|
||||||
controllerPre,
|
listener,
|
||||||
true,
|
true,
|
||||||
) {
|
) {
|
||||||
val sourceManager: SourceManager by injectLazy()
|
val sourceManager: SourceManager by injectLazy()
|
||||||
@ -18,32 +17,4 @@ class MigrationSourceAdapter(
|
|||||||
// SY _->
|
// SY _->
|
||||||
val sourcePreferences: SourcePreferences by injectLazy()
|
val sourcePreferences: SourcePreferences by injectLazy()
|
||||||
// SY <--
|
// 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
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
||||||
|
|
||||||
import android.os.Bundle
|
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.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.os.bundleOf
|
||||||
import androidx.core.view.ViewCompat
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.bluelinelabs.conductor.Router
|
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.BasicFullComposeController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
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.MigrationListController
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
|
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) :
|
class PreMigrationController(bundle: Bundle? = null) : BasicFullComposeController(bundle) {
|
||||||
BasicFullComposeController(bundle),
|
|
||||||
FlexibleAdapter.OnItemClickListener,
|
|
||||||
StartMigrationListener {
|
|
||||||
|
|
||||||
constructor(mangaIds: List<Long>) : this(
|
constructor(mangaIds: List<Long>) : this(
|
||||||
bundleOf(
|
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 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
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
Navigator(screen = PreMigrationScreen(config.toList()))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -349,7 +36,7 @@ class PreMigrationController(bundle: Bundle? = null) :
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PreMigrationController(mangaIds)
|
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
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||||
|
|
||||||
import android.os.Bundle
|
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.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shadow
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||||
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
|
|
||||||
|
|
||||||
class MigrationListController(bundle: Bundle? = null) :
|
class MigrationListController(bundle: Bundle? = null) :
|
||||||
FullComposeController<MigrationListPresenter>(bundle) {
|
BasicFullComposeController(bundle) {
|
||||||
|
|
||||||
constructor(config: MigrationProcedureConfig) : this(
|
constructor(config: MigrationProcedureConfig) : this(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
@ -86,414 +16,11 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val config = args.getParcelableCompat<MigrationProcedureConfig>(CONFIG_EXTRA)
|
val config = args.getSerializableCompat<MigrationProcedureConfig>(CONFIG_EXTRA)!!
|
||||||
|
|
||||||
private var selectedMangaId: Long? = null
|
|
||||||
private var manualMigrations = 0
|
|
||||||
|
|
||||||
override fun createPresenter(): MigrationListPresenter {
|
|
||||||
return MigrationListPresenter(config!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
val items by presenter.migratingItems.collectAsState()
|
Navigator(screen = MigrationListScreen(config))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.content.Context
|
||||||
import android.widget.Toast
|
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.UnsortedPreferences
|
||||||
import eu.kanade.domain.category.interactor.GetCategories
|
import eu.kanade.domain.category.interactor.GetCategories
|
||||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
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.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
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.MigrationFlags
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
|
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.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
@ -42,10 +42,11 @@ import exh.eh.EHentaiThrottleManager
|
|||||||
import exh.smartsearch.SmartSearchEngine
|
import exh.smartsearch.SmartSearchEngine
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
@ -55,7 +56,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class MigrationListPresenter(
|
class MigrationListScreenModel(
|
||||||
private val config: MigrationProcedureConfig,
|
private val config: MigrationProcedureConfig,
|
||||||
private val preferences: UnsortedPreferences = Injekt.get(),
|
private val preferences: UnsortedPreferences = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
@ -74,56 +75,56 @@ class MigrationListPresenter(
|
|||||||
private val getTracks: GetTracks = Injekt.get(),
|
private val getTracks: GetTracks = Injekt.get(),
|
||||||
private val insertTrack: InsertTrack = Injekt.get(),
|
private val insertTrack: InsertTrack = Injekt.get(),
|
||||||
private val deleteTrack: DeleteTrack = Injekt.get(),
|
private val deleteTrack: DeleteTrack = Injekt.get(),
|
||||||
) : BasePresenter<MigrationListController>() {
|
) : ScreenModel {
|
||||||
|
|
||||||
private val smartSearchEngine = SmartSearchEngine(config.extraSearchParams)
|
private val smartSearchEngine = SmartSearchEngine(config.extraSearchParams)
|
||||||
private val throttleManager = EHentaiThrottleManager()
|
private val throttleManager = EHentaiThrottleManager()
|
||||||
|
|
||||||
var migrationsJob: Job? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
val migratingItems = MutableStateFlow<List<MigratingManga>>(emptyList())
|
val migratingItems = MutableStateFlow<List<MigratingManga>>(emptyList())
|
||||||
val migrationDone = MutableStateFlow(false)
|
val migrationDone = MutableStateFlow(false)
|
||||||
val unfinishedCount = MutableStateFlow(0)
|
val unfinishedCount = MutableStateFlow(0)
|
||||||
|
|
||||||
|
val manualMigrations = MutableStateFlow(0)
|
||||||
|
|
||||||
val hideNotFound = preferences.hideNotFoundMigration().get()
|
val hideNotFound = preferences.hideNotFoundMigration().get()
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
val navigateOut = MutableSharedFlow<Unit>()
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
if (migrationsJob?.isActive != true) {
|
val dialog = MutableStateFlow<Dialog?>(null)
|
||||||
migrationsJob = presenterScope.launchIO {
|
|
||||||
runMigrations(
|
init {
|
||||||
config.mangaIds
|
coroutineScope.launchIO {
|
||||||
.map {
|
runMigrations(
|
||||||
async {
|
config.mangaIds
|
||||||
val manga = getManga.await(it) ?: return@async null
|
.map {
|
||||||
MigratingManga(
|
async {
|
||||||
manga = manga,
|
val manga = getManga.await(it) ?: return@async null
|
||||||
chapterInfo = getChapterInfo(it),
|
MigratingManga(
|
||||||
sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
|
manga = manga,
|
||||||
if (manga.source == MERGED_SOURCE_ID) {
|
chapterInfo = getChapterInfo(it),
|
||||||
getMergedReferencesById.await(manga.id)
|
sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
|
||||||
.map { sourceManager.getOrStub(it.mangaSourceId) }
|
if (manga.source == MERGED_SOURCE_ID) {
|
||||||
} else {
|
getMergedReferencesById.await(manga.id)
|
||||||
null
|
.map { sourceManager.getOrStub(it.mangaSourceId) }
|
||||||
},
|
} else {
|
||||||
),
|
null
|
||||||
parentContext = presenterScope.coroutineContext,
|
},
|
||||||
)
|
),
|
||||||
}
|
parentContext = coroutineScope.coroutineContext,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.awaitAll()
|
}
|
||||||
.filterNotNull()
|
.awaitAll()
|
||||||
.also {
|
.filterNotNull()
|
||||||
migratingItems.value = it
|
.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(result: SearchResult.Result) = getChapterInfo(result.id)
|
||||||
suspend fun getChapterInfo(id: Long) = getChapterByMangaId.await(id).let { chapters ->
|
suspend fun getChapterInfo(id: Long) = getChapterByMangaId.await(id).let { chapters ->
|
||||||
MigratingManga.ChapterInfo(
|
MigratingManga.ChapterInfo(
|
||||||
@ -146,7 +147,7 @@ class MigrationListPresenter(
|
|||||||
|
|
||||||
val sources = getMigrationSources()
|
val sources = getMigrationSources()
|
||||||
for (manga in mangas) {
|
for (manga in mangas) {
|
||||||
if (migrationsJob?.isCancelled == true) {
|
if (!currentCoroutineContext().isActive) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// in case it was removed
|
// in case it was removed
|
||||||
@ -224,7 +225,7 @@ class MigrationListPresenter(
|
|||||||
source.getChapterList(localManga.toSManga())
|
source.getChapterList(localManga.toSManga())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
this@MigrationListPresenter.logcat(LogPriority.ERROR, e)
|
this@MigrationListScreenModel.logcat(LogPriority.ERROR, e)
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
syncChaptersWithSource.await(chapters, localManga, source)
|
syncChaptersWithSource.await(chapters, localManga, source)
|
||||||
@ -274,14 +275,13 @@ class MigrationListPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sourceFinished() {
|
private fun sourceFinished() {
|
||||||
unfinishedCount.value = migratingItems.value.count {
|
unfinishedCount.value = migratingItems.value.count {
|
||||||
it.searchResult.value != SearchResult.Searching
|
it.searchResult.value != SearchResult.Searching
|
||||||
}
|
}
|
||||||
if (allMangasDone()) {
|
if (allMangasDone()) {
|
||||||
migrationDone.value = true
|
migrationDone.value = true
|
||||||
}
|
}
|
||||||
withUIContext { view?.sourceFinished() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun allMangasDone() = migratingItems.value.all { it.searchResult.value != SearchResult.Searching } &&
|
fun allMangasDone() = migratingItems.value.all { it.searchResult.value != SearchResult.Searching } &&
|
||||||
@ -383,11 +383,11 @@ class MigrationListPresenter(
|
|||||||
updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate))
|
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 }
|
val migratingManga = migratingItems.value.find { it.manga.id == selectedMangaId }
|
||||||
?: return
|
?: return
|
||||||
migratingManga.searchResult.value = SearchResult.Searching
|
migratingManga.searchResult.value = SearchResult.Searching
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
val result = migratingManga.migrationScope.async {
|
val result = migratingManga.migrationScope.async {
|
||||||
val localManga = networkToLocalManga.await(manga)
|
val localManga = networkToLocalManga.await(manga)
|
||||||
try {
|
try {
|
||||||
@ -413,14 +413,14 @@ class MigrationListPresenter(
|
|||||||
} else {
|
} else {
|
||||||
migratingManga.searchResult.value = SearchResult.NotFound
|
migratingManga.searchResult.value = SearchResult.NotFound
|
||||||
withUIContext {
|
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() {
|
fun migrateMangas() {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
migratingItems.value.forEach { manga ->
|
migratingItems.value.forEach { manga ->
|
||||||
val searchResult = manga.searchResult.value
|
val searchResult = manga.searchResult.value
|
||||||
if (searchResult is SearchResult.Result) {
|
if (searchResult is SearchResult.Result) {
|
||||||
@ -438,7 +438,7 @@ class MigrationListPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun copyMangas() {
|
fun copyMangas() {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
migratingItems.value.forEach { manga ->
|
migratingItems.value.forEach { manga ->
|
||||||
val searchResult = manga.searchResult.value
|
val searchResult = manga.searchResult.value
|
||||||
if (searchResult is SearchResult.Result) {
|
if (searchResult is SearchResult.Result) {
|
||||||
@ -455,26 +455,12 @@ class MigrationListPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun navigateOut() {
|
private suspend fun navigateOut() {
|
||||||
val view = view ?: return
|
navigateOut.emit(Unit)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun migrateManga(mangaId: Long, copy: Boolean) {
|
fun migrateManga(mangaId: Long, copy: Boolean) {
|
||||||
presenterScope.launchIO {
|
manualMigrations.value++
|
||||||
|
coroutineScope.launchIO {
|
||||||
val manga = migratingItems.value.find { it.manga.id == mangaId }
|
val manga = migratingItems.value.find { it.manga.id == mangaId }
|
||||||
?: return@launchIO
|
?: return@launchIO
|
||||||
|
|
||||||
@ -491,7 +477,7 @@ class MigrationListPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeManga(mangaId: Long) {
|
fun removeManga(mangaId: Long) {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
val item = migratingItems.value.find { it.manga.id == mangaId }
|
val item = migratingItems.value.find { it.manga.id == mangaId }
|
||||||
?: return@launchIO
|
?: return@launchIO
|
||||||
if (migratingItems.value.size == 1) {
|
if (migratingItems.value.size == 1) {
|
||||||
@ -517,11 +503,25 @@ class MigrationListPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDispose() {
|
||||||
super.onDestroy()
|
super.onDispose()
|
||||||
migrationsJob?.cancel()
|
|
||||||
migratingItems.value.forEach {
|
migratingItems.value.forEach {
|
||||||
it.migrationScope.cancel()
|
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
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||||
|
|
||||||
import android.os.Parcelable
|
import java.io.Serializable
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class MigrationProcedureConfig(
|
data class MigrationProcedureConfig(
|
||||||
var mangaIds: List<Long>,
|
var mangaIds: List<Long>,
|
||||||
val extraSearchParams: String?,
|
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.interactor.GetManga
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
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.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -24,24 +24,23 @@ class SearchController(
|
|||||||
SOURCES to sources?.map { it.id }?.toLongArray(),
|
SOURCES to sources?.map { it.id }?.toLongArray(),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
constructor(targetController: MigrationListController?, mangaId: Long, sources: LongArray) :
|
constructor(mangaId: Long, sources: LongArray) :
|
||||||
this(
|
this(
|
||||||
runBlocking {
|
runBlocking {
|
||||||
Injekt.get<GetManga>()
|
Injekt.get<GetManga>()
|
||||||
.await(mangaId)
|
.await(mangaId)
|
||||||
},
|
},
|
||||||
sources.map { Injekt.get<SourceManager>().getOrStub(it) }.filterIsInstance<CatalogueSource>(),
|
sources.map { Injekt.get<SourceManager>().getOrStub(it) }.filterIsInstance<CatalogueSource>(),
|
||||||
) {
|
)
|
||||||
this.targetController = targetController
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
constructor(bundle: Bundle) : this(
|
constructor(bundle: Bundle) : this(
|
||||||
null,
|
|
||||||
bundle.getLong(OLD_MANGA),
|
bundle.getLong(OLD_MANGA),
|
||||||
bundle.getLongArray(SOURCES) ?: LongArray(0),
|
bundle.getLongArray(SOURCES) ?: LongArray(0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var useMangaForMigration: ((Manga, Source) -> Unit)? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when controller is initialized.
|
* Called when controller is initialized.
|
||||||
*/
|
*/
|
||||||
@ -58,10 +57,9 @@ class SearchController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMangaClick(manga: Manga) {
|
override fun onMangaClick(manga: Manga) {
|
||||||
val migrationListController = targetController as MigrationListController
|
|
||||||
val sourceManager = Injekt.get<SourceManager>()
|
val sourceManager = Injekt.get<SourceManager>()
|
||||||
val source = sourceManager.get(manga.source) ?: return
|
val source = sourceManager.get(manga.source) ?: return
|
||||||
migrationListController.useMangaForMigration(manga, source)
|
useMangaForMigration?.let { it(manga, source) }
|
||||||
router.popCurrentController()
|
router.popCurrentController()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +71,7 @@ class SearchController(
|
|||||||
override fun onTitleClick(source: CatalogueSource) {
|
override fun onTitleClick(source: CatalogueSource) {
|
||||||
presenter.sourcePreferences.lastUsedSource().set(source.id)
|
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 {
|
companion object {
|
||||||
|
@ -7,9 +7,9 @@ import androidx.core.os.bundleOf
|
|||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.presentation.browse.SourceSearchScreen
|
import eu.kanade.presentation.browse.SourceSearchScreen
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
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.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -19,15 +19,15 @@ class SourceSearchController(
|
|||||||
bundle: Bundle,
|
bundle: Bundle,
|
||||||
) : BrowseSourceController(bundle) {
|
) : BrowseSourceController(bundle) {
|
||||||
|
|
||||||
constructor(targetController: MigrationListController, manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this(
|
constructor(manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
SOURCE_ID_KEY to source.id,
|
SOURCE_ID_KEY to source.id,
|
||||||
MANGA_KEY to manga,
|
MANGA_KEY to manga,
|
||||||
SEARCH_QUERY_KEY to searchQuery,
|
SEARCH_QUERY_KEY to searchQuery,
|
||||||
),
|
),
|
||||||
) {
|
)
|
||||||
this.targetController = targetController
|
|
||||||
}
|
var useMangaForMigration: ((Manga, Source) -> Unit)? = null
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
@ -37,11 +37,11 @@ class SourceSearchController(
|
|||||||
onFabClick = { filterSheet?.show() },
|
onFabClick = { filterSheet?.show() },
|
||||||
// SY -->
|
// SY -->
|
||||||
onMangaClick = { manga ->
|
onMangaClick = { manga ->
|
||||||
val migrationListController = targetController as? MigrationListController ?: return@SourceSearchScreen
|
|
||||||
val sourceManager = Injekt.get<SourceManager>()
|
val sourceManager = Injekt.get<SourceManager>()
|
||||||
val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
|
val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
|
||||||
migrationListController.useMangaForMigration(manga, source)
|
useMangaForMigration?.let { it(manga, source) }
|
||||||
router.popToTag(MigrationListController.TAG)
|
router.popCurrentController()
|
||||||
|
router.popCurrentController()
|
||||||
},
|
},
|
||||||
// SY <--
|
// SY <--
|
||||||
onWebViewClick = f@{
|
onWebViewClick = f@{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user