Use Voyager for migration
This commit is contained in:
@@ -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 ""),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user