komikku/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt

402 lines
13 KiB
Kotlin
Raw Normal View History

2022-08-31 21:20:29 +02:00
package eu.kanade.presentation.browse
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
2022-08-31 21:20:29 +02:00
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
2022-08-31 21:20:29 +02:00
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
2022-08-31 21:20:29 +02:00
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
2022-08-31 21:20:29 +02:00
import androidx.compose.material.ContentAlpha
import androidx.compose.material3.AlertDialog
2022-08-31 21:20:29 +02:00
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
2022-08-31 21:20:29 +02:00
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
2022-08-31 21:20:29 +02:00
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
2022-08-31 21:20:29 +02:00
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
2022-08-31 21:20:29 +02:00
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.plus
2022-11-16 19:52:17 +01:00
import eu.kanade.presentation.util.topSmallPaddingValues
2022-08-31 21:20:29 +02:00
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.feed.FeedPresenter
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import eu.kanade.domain.manga.model.MangaCover as MangaCoverData
data class FeedItemUI(
val feed: FeedSavedSearch,
val savedSearch: SavedSearch?,
val source: CatalogueSource?,
val title: String,
val subtitle: String,
val results: List<Manga>?,
)
@Composable
fun FeedScreen(
presenter: FeedPresenter,
contentPadding: PaddingValues,
onClickAdd: (CatalogueSource) -> Unit,
onClickCreate: (CatalogueSource, SavedSearch?) -> Unit,
2022-08-31 21:20:29 +02:00
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickDeleteConfirm: (FeedSavedSearch) -> Unit,
2022-08-31 21:20:29 +02:00
onClickManga: (Manga) -> Unit,
) {
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
textResource = R.string.feed_tab_empty,
modifier = Modifier.padding(contentPadding),
)
2022-08-31 21:20:29 +02:00
else -> {
FeedList(
state = presenter,
contentPadding = contentPadding,
getMangaState = { item, source -> presenter.getManga(item, source) },
2022-08-31 21:20:29 +02:00
onClickSavedSearch = onClickSavedSearch,
onClickSource = onClickSource,
onClickDelete = onClickDelete,
onClickManga = onClickManga,
)
}
}
2022-10-09 02:46:41 +02:00
when (val dialog = presenter.dialog) {
is FeedPresenter.Dialog.AddFeed -> {
FeedAddDialog(
sources = dialog.options,
onDismiss = { presenter.dialog = null },
onClickAdd = {
presenter.dialog = null
onClickAdd(it ?: return@FeedAddDialog)
},
)
}
is FeedPresenter.Dialog.AddFeedSearch -> {
FeedAddSearchDialog(
source = dialog.source,
savedSearches = dialog.options,
onDismiss = { presenter.dialog = null },
onClickAdd = { source, savedSearch ->
presenter.dialog = null
onClickCreate(source, savedSearch)
},
)
}
is FeedPresenter.Dialog.DeleteFeed -> {
FeedDeleteConfirmDialog(
feed = dialog.feed,
onDismiss = { presenter.dialog = null },
onClickDeleteConfirm = {
presenter.dialog = null
onClickDeleteConfirm(it)
},
)
}
null -> Unit
}
2022-08-31 21:20:29 +02:00
}
@Composable
fun FeedList(
state: FeedState,
contentPadding: PaddingValues,
getMangaState: @Composable ((Manga, CatalogueSource?) -> State<Manga>),
2022-08-31 21:20:29 +02:00
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickManga: (Manga) -> Unit,
) {
ScrollbarLazyColumn(
2022-11-16 19:52:17 +01:00
contentPadding = contentPadding + topSmallPaddingValues,
2022-08-31 21:20:29 +02:00
) {
items(
state.items.orEmpty(),
key = { it.feed.id },
) { item ->
FeedItem(
modifier = Modifier.animateItemPlacement(),
item = item,
getMangaState = { getMangaState(it, item.source) },
2022-08-31 21:20:29 +02:00
onClickSavedSearch = onClickSavedSearch,
onClickSource = onClickSource,
onClickDelete = onClickDelete,
onClickManga = onClickManga,
)
}
}
}
@Composable
fun FeedItem(
modifier: Modifier,
item: FeedItemUI,
getMangaState: @Composable ((Manga) -> State<Manga>),
2022-08-31 21:20:29 +02:00
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickManga: (Manga) -> Unit,
) {
Column(
modifier then Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Row(
Modifier
.fillMaxWidth()
.combinedClickable(
onLongClick = {
onClickDelete(item.feed)
},
onClick = {
if (item.savedSearch != null && item.source != null) {
onClickSavedSearch(item.savedSearch, item.source)
} else if (item.source != null) {
onClickSource(item.source)
}
},
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.padding(start = 16.dp)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = item.subtitle,
style = MaterialTheme.typography.bodyMedium,
fontSize = 12.sp,
color = LocalContentColor.current.copy(alpha = ContentAlpha.high),
)
}
Icon(
painter = painterResource(R.drawable.ic_arrow_forward_24dp),
contentDescription = stringResource(R.string.label_more),
modifier = Modifier.padding(16.dp),
)
}
when {
item.results == null -> {
CircularProgressIndicator()
}
item.results.isEmpty() -> {
Text(stringResource(R.string.no_results_found), modifier = Modifier.padding(bottom = 16.dp))
}
else -> {
LazyRow(
Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 12.dp),
) {
items(item.results) {
val manga by getMangaState(it)
2022-08-31 21:20:29 +02:00
FeedCardItem(
manga = manga,
2022-08-31 21:20:29 +02:00
onClickManga = onClickManga,
)
}
}
}
}
}
}
@Composable
fun FeedCardItem(
modifier: Modifier = Modifier,
manga: Manga,
onClickManga: (Manga) -> Unit,
) {
Column(
modifier
.padding(vertical = 4.dp)
.width(112.dp)
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = { onClickManga(manga) })
.padding(4.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(MangaCover.Book.ratio),
) {
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
2022-08-31 21:20:29 +02:00
.alpha(
if (manga.favorite) 0.3f else 1.0f,
),
data = MangaCoverData(
manga.id,
manga.source,
manga.favorite,
manga.thumbnailUrl,
manga.coverLastModified,
),
)
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
) {
if (manga.favorite) {
Badge(text = stringResource(R.string.in_library))
}
}
}
Text(
modifier = Modifier.padding(4.dp),
text = manga.title,
fontSize = 12.sp,
maxLines = 2,
style = MaterialTheme.typography.titleSmall,
)
}
}
@Composable
fun FeedAddDialog(
sources: List<CatalogueSource>,
onDismiss: () -> Unit,
onClickAdd: (CatalogueSource?) -> Unit,
) {
var selected by remember { mutableStateOf<Int?>(null) }
AlertDialog(
title = {
Text(text = stringResource(R.string.feed))
},
text = {
RadioSelector(options = sources, selected = selected) {
selected = it
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickAdd(selected?.let { sources[it] }) }) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
@Composable
fun FeedAddSearchDialog(
source: CatalogueSource,
savedSearches: List<SavedSearch?>,
onDismiss: () -> Unit,
onClickAdd: (CatalogueSource, SavedSearch?) -> Unit,
) {
var selected by remember { mutableStateOf<Int?>(null) }
AlertDialog(
title = {
Text(text = source.name)
},
text = {
val context = LocalContext.current
val savedSearchStrings = remember {
savedSearches.map {
it?.name ?: context.getString(R.string.latest)
}
}
RadioSelector(
options = savedSearches,
optionStrings = savedSearchStrings,
selected = selected,
) {
selected = it
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickAdd(source, selected?.let { savedSearches[it] }) }) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
@Composable
fun <T> RadioSelector(
options: List<T>,
optionStrings: List<String> = remember { options.map { it.toString() } },
selected: Int?,
onSelectOption: (Int) -> Unit,
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
optionStrings.forEachIndexed { index, option ->
Row(
Modifier
.fillMaxWidth()
.height(48.dp)
.clickable { onSelectOption(index) },
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(selected == index, onClick = null)
Spacer(Modifier.width(4.dp))
Text(option, maxLines = 1)
}
}
}
}
@Composable
fun FeedDeleteConfirmDialog(
feed: FeedSavedSearch,
onDismiss: () -> Unit,
onClickDeleteConfirm: (FeedSavedSearch) -> Unit,
) {
AlertDialog(
title = {
Text(text = stringResource(R.string.feed))
},
text = {
Text(text = stringResource(R.string.feed_delete))
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickDeleteConfirm(feed) }) {
Text(text = stringResource(R.string.action_delete))
}
},
)
}