Merge branch 'tachiyomiorg:master' into range_selection

This commit is contained in:
d-najd 2022-10-10 22:59:10 +02:00 committed by GitHub
commit f0bc8a97ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 589 additions and 626 deletions

View File

@ -315,6 +315,7 @@ tasks {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
@ -327,8 +328,6 @@ tasks {
)
}
preBuild {
val ktlintTask = if (System.getenv("GITHUB_BASE_REF") == null) formatKotlin else lintKotlin
dependsOn(ktlintTask)

View File

@ -13,6 +13,9 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.NewReleases
@ -49,6 +52,7 @@ import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
@ -56,7 +60,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.widget.EmptyView
@Composable
fun BrowseSourceScreen(
@ -248,13 +251,29 @@ fun BrowseSourceContent(
message = getErrorMessage(errorState),
actions = if (state.source is LocalSource) {
listOf(
EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { onLocalSourceHelpClick() },
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
icon = Icons.Default.HelpOutline,
onClick = onLocalSourceHelpClick,
),
)
} else {
listOf(
EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp) { mangaList.refresh() },
EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { onWebViewClick() },
EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { onHelpClick() },
EmptyScreenAction(
stringResId = R.string.action_retry,
icon = Icons.Default.Refresh,
onClick = mangaList::refresh,
),
EmptyScreenAction(
stringResId = R.string.action_open_in_web_view,
icon = Icons.Default.Public,
onClick = onWebViewClick,
),
EmptyScreenAction(
stringResId = R.string.label_help,
icon = Icons.Default.HelpOutline,
onClick = onHelpClick,
),
)
},
)

View File

@ -133,7 +133,10 @@ private fun ExtensionDetails(
) {
when {
presenter.isLoading -> LoadingScreen()
presenter.extension == null -> EmptyScreen(textResource = R.string.empty_screen)
presenter.extension == null -> EmptyScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
val context = LocalContext.current
val extension = presenter.extension

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
@ -37,7 +38,10 @@ fun ExtensionFilterScreen(
) { contentPadding ->
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.empty_screen)
presenter.isEmpty -> EmptyScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
SourceFilterContent(
contentPadding = contentPadding,

View File

@ -5,11 +5,9 @@ import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.combinedClickable
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.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
@ -37,14 +35,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding
@ -57,11 +53,11 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
@Composable
fun ExtensionScreen(
presenter: ExtensionsPresenter,
contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
@ -73,16 +69,20 @@ fun ExtensionScreen(
onRefresh: () -> Unit,
) {
SwipeRefresh(
state = rememberSwipeRefreshState(presenter.isRefreshing),
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
refreshing = presenter.isRefreshing,
onRefresh = onRefresh,
enabled = !presenter.isLoading,
) {
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(R.string.empty_screen)
presenter.isEmpty -> EmptyScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
ExtensionContent(
state = presenter,
contentPadding = contentPadding,
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,
onInstallExtension = onInstallExtension,
@ -100,6 +100,7 @@ fun ExtensionScreen(
@Composable
private fun ExtensionContent(
state: ExtensionsState,
contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
@ -112,7 +113,7 @@ private fun ExtensionContent(
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
FastScrollLazyColumn(
contentPadding = bottomNavPadding + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
contentPadding = contentPadding + topPaddingValues,
) {
items(
items = state.items,

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -39,7 +40,10 @@ fun MigrateMangaScreen(
) { contentPadding ->
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.empty_screen)
presenter.isEmpty -> EmptyScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
MigrateMangaContent(
contentPadding = contentPadding,

View File

@ -3,10 +3,8 @@ package eu.kanade.presentation.browse
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
@ -34,27 +32,33 @@ import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
@Composable
fun MigrateSourceScreen(
presenter: MigrationSourcesPresenter,
contentPadding: PaddingValues,
onClickItem: (Source) -> Unit,
) {
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library)
presenter.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding),
)
else ->
MigrateSourceList(
list = presenter.items,
contentPadding = contentPadding,
onClickItem = onClickItem,
onLongClickItem = { source ->
val sourceId = source.id.toString()
@ -71,6 +75,7 @@ fun MigrateSourceScreen(
@Composable
private fun MigrateSourceList(
list: List<Pair<Source, Long>>,
contentPadding: PaddingValues,
onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,
sortingMode: SetMigrateSorting.Mode,
@ -79,9 +84,9 @@ private fun MigrateSourceList(
onToggleSortingDirection: () -> Unit,
) {
ScrollbarLazyColumn(
contentPadding = bottomNavPadding + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
contentPadding = contentPadding + topPaddingValues,
) {
stickyHeader(key = "header") {
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
Row(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
@ -162,6 +167,7 @@ private fun MigrateSourceItem(
) {
if (sourceLangString != null) {
Text(
modifier = Modifier.secondaryItemAlpha(),
text = sourceLangString,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -170,6 +176,7 @@ private fun MigrateSourceItem(
}
if (source.isStub) {
Text(
modifier = Modifier.secondaryItemAlpha(),
text = stringResource(R.string.not_installed),
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Switch
@ -43,7 +44,10 @@ fun SourcesFilterScreen(
) { contentPadding ->
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.source_filter_empty_screen)
presenter.isEmpty -> EmptyScreen(
textResource = R.string.source_filter_empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
SourcesFilterContent(
contentPadding = contentPadding,

View File

@ -2,10 +2,8 @@ package eu.kanade.presentation.browse
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
@ -40,12 +38,12 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
import kotlinx.coroutines.flow.collectLatest
@Composable
fun SourcesScreen(
presenter: SourcesPresenter,
contentPadding: PaddingValues,
onClickItem: (Source, String) -> Unit,
onClickDisable: (Source) -> Unit,
onClickPin: (Source) -> Unit,
@ -53,10 +51,14 @@ fun SourcesScreen(
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen)
presenter.isEmpty -> EmptyScreen(
textResource = R.string.source_empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
SourceList(
state = presenter,
contentPadding = contentPadding,
onClickItem = onClickItem,
onClickDisable = onClickDisable,
onClickPin = onClickPin,
@ -77,12 +79,13 @@ fun SourcesScreen(
@Composable
private fun SourceList(
state: SourcesState,
contentPadding: PaddingValues,
onClickItem: (Source, String) -> Unit,
onClickDisable: (Source) -> Unit,
onClickPin: (Source) -> Unit,
) {
ScrollbarLazyColumn(
contentPadding = bottomNavPadding + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
contentPadding = contentPadding + topPaddingValues,
) {
items(
items = state.items,
@ -187,11 +190,12 @@ private fun SourcePinButton(
) {
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground
val description = if (isPinned) R.string.action_unpin else R.string.action_pin
IconButton(onClick = onClick) {
Icon(
imageVector = icon,
contentDescription = "",
tint = tint,
contentDescription = stringResource(description),
)
}
}

View File

@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable
@ -53,6 +54,7 @@ private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { s
)
if (sourceLangString != null) {
Text(
modifier = Modifier.secondaryItemAlpha(),
text = sourceLangString,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View File

@ -49,7 +49,7 @@ fun SourceIcon(
source.isStub && icon == null -> {
Image(
imageVector = Icons.Default.Warning,
contentDescription = "",
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
modifier = modifier.then(defaultModifier),
)
@ -57,14 +57,14 @@ fun SourceIcon(
icon != null -> {
Image(
bitmap = icon,
contentDescription = "",
contentDescription = null,
modifier = modifier.then(defaultModifier),
)
}
else -> {
Image(
painter = painterResource(id = R.mipmap.ic_local_source),
contentDescription = "",
contentDescription = null,
modifier = modifier.then(defaultModifier),
)
}

View File

@ -1,18 +1,20 @@
package eu.kanade.presentation.browse.components
import androidx.compose.material.TextButton
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R
@Composable
fun RemoveMangaDialog(
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
mangaToRemove: Manga,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
@ -35,7 +37,7 @@ fun RemoveMangaDialog(
Text(text = stringResource(R.string.are_you_sure))
},
text = {
Text(text = stringResource(R.string.remove_manga))
Text(text = stringResource(R.string.remove_manga, mangaToRemove.title))
},
)
}

View File

@ -1,9 +1,11 @@
package eu.kanade.presentation.category
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.category.components.CategoryContent
@ -48,7 +50,10 @@ fun CategoryScreen(
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_category)
presenter.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_category,
modifier = Modifier.padding(paddingValues),
)
else -> {
CategoryContent(
state = presenter,

View File

@ -3,6 +3,7 @@ package eu.kanade.presentation.category.components
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
@ -12,7 +13,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.components.TextButton
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.delay

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
@ -21,7 +22,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
@ -44,6 +44,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
@Composable
@ -266,6 +267,7 @@ fun SearchToolbar(
placeholder = {
if (!placeholderText.isNullOrEmpty()) {
Text(
modifier = Modifier.secondaryItemAlpha(),
text = placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View File

@ -41,6 +41,11 @@ import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.animateElevation
import androidx.compose.material3.ButtonDefaults as M3ButtonDefaults
/**
* TextButton with additional onLongClick functionality.
*
* @see androidx.compose.material3.TextButton
*/
@Composable
fun TextButton(
onClick: () -> Unit,
@ -74,6 +79,11 @@ fun TextButton(
content = content,
)
/**
* Button with additional onLongClick functionality.
*
* @see androidx.compose.material3.TextButton
*/
@Composable
fun Button(
onClick: () -> Unit,

View File

@ -1,23 +1,49 @@
package eu.kanade.presentation.components
import android.view.ViewGroup
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.widget.EmptyView
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
import kotlin.random.Random
@Composable
fun EmptyScreen(
@StringRes textResource: Int,
actions: List<EmptyView.Action>? = null,
modifier: Modifier = Modifier,
actions: List<EmptyScreenAction>? = null,
) {
EmptyScreen(
message = stringResource(textResource),
modifier = modifier,
actions = actions,
)
}
@ -25,24 +51,174 @@ fun EmptyScreen(
@Composable
fun EmptyScreen(
message: String,
actions: List<EmptyView.Action>? = null,
modifier: Modifier = Modifier,
actions: List<EmptyScreenAction>? = null,
) {
Box(
modifier = Modifier
.fillMaxSize(),
) {
AndroidView(
factory = { context ->
EmptyView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
show(message, actions)
val face = remember { getRandomErrorFace() }
Layout(
content = {
Column(
modifier = Modifier
.layoutId("face")
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = face,
modifier = Modifier.secondaryItemAlpha(),
fontFamily = FontFamily.Monospace,
style = MaterialTheme.typography.displayMedium,
)
Text(
text = message,
modifier = Modifier.paddingFromBaseline(top = 24.dp),
style = MaterialTheme.typography.bodyMedium,
)
}
if (!actions.isNullOrEmpty()) {
Row(
modifier = Modifier
.layoutId("actions")
.padding(
top = 24.dp,
start = horizontalPadding,
end = horizontalPadding,
),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
actions.forEach {
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(id = it.stringResId),
icon = it.icon,
onClick = it.onClick,
)
}
}
},
modifier = Modifier
.align(Alignment.Center),
)
}
},
modifier = modifier.fillMaxSize(),
) { measurables, constraints ->
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val facePlaceable = measurables.first { it.layoutId == "face" }
.measure(looseConstraints)
val actionsPlaceable = measurables.firstOrNull { it.layoutId == "actions" }
?.measure(looseConstraints)
layout(constraints.maxWidth, constraints.maxHeight) {
val faceY = (constraints.maxHeight - facePlaceable.height) / 2
facePlaceable.placeRelative(
x = (constraints.maxWidth - facePlaceable.width) / 2,
y = faceY,
)
actionsPlaceable?.placeRelative(
x = (constraints.maxWidth - actionsPlaceable.width) / 2,
y = faceY + facePlaceable.height,
)
}
}
}
@Composable
private fun ActionButton(
modifier: Modifier = Modifier,
title: String,
icon: ImageVector,
onClick: () -> Unit,
) {
TextButton(
modifier = modifier,
onClick = onClick,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = icon,
contentDescription = null,
)
Spacer(Modifier.height(4.dp))
Text(
text = title,
textAlign = TextAlign.Center,
)
}
}
}
@Preview(
name = "Light",
widthDp = 400,
heightDp = 400,
)
@Preview(
name = "Dark",
widthDp = 400,
heightDp = 400,
uiMode = UI_MODE_NIGHT_YES,
)
@Composable
private fun NoActionPreview() {
TachiyomiTheme {
Surface {
EmptyScreen(
textResource = R.string.empty_screen,
)
}
}
}
@Preview(
name = "Light",
widthDp = 400,
heightDp = 400,
)
@Preview(
name = "Dark",
widthDp = 400,
heightDp = 400,
uiMode = UI_MODE_NIGHT_YES,
)
@Composable
private fun WithActionPreview() {
TachiyomiTheme {
Surface {
EmptyScreen(
textResource = R.string.empty_screen,
actions = listOf(
EmptyScreenAction(
stringResId = R.string.action_retry,
icon = Icons.Default.Refresh,
onClick = {},
),
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.Default.HelpOutline,
onClick = {},
),
),
)
}
}
}
data class EmptyScreenAction(
@StringRes val stringResId: Int,
val icon: ImageVector,
val onClick: () -> Unit,
)
private val horizontalPadding = 24.dp
private val ERROR_FACES = listOf(
"(・o・;)",
"Σ(ಠ_ಠ)",
"ಥ_ಥ",
"(˘・_・˘)",
"(; ̄Д ̄)",
"(・Д・。",
)
private fun getRandomErrorFace(): String {
return ERROR_FACES[Random.nextInt(ERROR_FACES.size)]
}

View File

@ -28,6 +28,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
/**
* ExtendedFloatingActionButton with custom transition between collapsed/expanded state.
*
* @see androidx.compose.material3.ExtendedFloatingActionButton
*/
@Composable
fun ExtendedFloatingActionButton(
text: @Composable () -> Unit,

View File

@ -1,192 +1,12 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.kanade.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.minimumTouchTargetSize
/**
* <a href="https://m3.material.io/components/icon-button/overview" class="external" target="_blank">Material Design standard icon button</a>.
* Exposing some internal tokens.
*
* Icon buttons help people take supplementary actions with a single tap. Theyre used when a
* compact button is required, such as in a toolbar or image list.
*
* ![Standard icon button image](https://developer.android.com/images/reference/androidx/compose/material3/standard-icon-button.png)
*
* [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
* custom icon, note that the typical size for the internal icon is 24 x 24 dp.
* This icon button has an overall minimum touch target size of 48 x 48dp, to meet accessibility
* guidelines.
*
* @sample androidx.compose.material3.samples.IconButtonSample
*
* Tachiyomi changes:
* * Add on long click
*
* @param onClick called when this icon button is clicked
* @param modifier the [Modifier] to be applied to this icon button
* @param enabled controls the enabled state of this icon button. When `false`, this component will
* not respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this icon button. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this icon button in different states.
* @param colors [IconButtonColors] that will be used to resolve the colors used for this icon
* button in different states. See [IconButtonDefaults.iconButtonColors].
* @param content the content of this icon button, typically an [Icon]
* @see androidx.compose.material3.tokens.IconButtonTokens
*/
@Composable
fun IconButton(
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
content: @Composable () -> Unit,
) {
Box(
modifier =
modifier
.minimumTouchTargetSize()
.size(IconButtonTokens.StateLayerSize)
.background(color = colors.containerColor(enabled).value)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
),
contentAlignment = Alignment.Center,
) {
val contentColor = colors.contentColor(enabled).value
CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
}
}
object IconButtonDefaults {
/**
* Creates a [IconButtonColors] that represents the default colors used in a [IconButton].
*
* @param containerColor the container color of this icon button when enabled.
* @param contentColor the content color of this icon button when enabled.
* @param disabledContainerColor the container color of this icon button when not enabled.
* @param disabledContentColor the content color of this icon button when not enabled.
*/
@Composable
fun iconButtonColors(
containerColor: Color = Color.Transparent,
contentColor: Color = LocalContentColor.current,
disabledContainerColor: Color = Color.Transparent,
disabledContentColor: Color = contentColor.copy(alpha = 0.38f),
): IconButtonColors =
IconButtonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
)
}
object IconButtonTokens {
val StateLayerSize = 40.0.dp
}
/**
* Represents the container and content colors used in an icon button in different states.
*
* - See [IconButtonDefaults.filledIconButtonColors] and
* [IconButtonDefaults.filledTonalIconButtonColors] for the default colors used in a
* [FilledIconButton].
* - See [IconButtonDefaults.outlinedIconButtonColors] for the default colors used in an
* [OutlinedIconButton].
*/
@Immutable
class IconButtonColors internal constructor(
private val containerColor: Color,
private val contentColor: Color,
private val disabledContainerColor: Color,
private val disabledContentColor: Color,
) {
/**
* Represents the container color for this icon button, depending on [enabled].
*
* @param enabled whether the icon button is enabled
*/
@Composable
internal fun containerColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor)
}
/**
* Represents the content color for this icon button, depending on [enabled].
*
* @param enabled whether the icon button is enabled
*/
@Composable
internal fun contentColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is IconButtonColors) return false
if (containerColor != other.containerColor) return false
if (contentColor != other.contentColor) return false
if (disabledContainerColor != other.disabledContainerColor) return false
if (disabledContentColor != other.disabledContentColor) return false
return true
}
override fun hashCode(): Int {
var result = containerColor.hashCode()
result = 31 * result + contentColor.hashCode()
result = 31 * result + disabledContainerColor.hashCode()
result = 31 * result + disabledContentColor.hashCode()
return result
}
}

View File

@ -1,6 +1,8 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@ -8,6 +10,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
@ -16,13 +19,14 @@ import androidx.compose.ui.unit.dp
fun Pill(
text: String,
modifier: Modifier = Modifier,
color: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.background,
contentColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onBackground,
color: Color = MaterialTheme.colorScheme.background,
contentColor: Color = MaterialTheme.colorScheme.onBackground,
elevation: Dp = 1.dp,
fontSize: TextUnit = LocalTextStyle.current.fontSize,
) {
androidx.compose.material3.Surface(
modifier = modifier
.requiredWidth(IntrinsicSize.Max)
.padding(start = 4.dp)
.clip(RoundedCornerShape(100)),
color = color,
@ -33,6 +37,8 @@ fun Pill(
text = text,
modifier = Modifier.padding(6.dp, 1.dp),
fontSize = fontSize,
maxLines = 1,
softWrap = false,
)
}
}

View File

@ -30,6 +30,11 @@ import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.minimumTouchTargetSize
import kotlin.math.ln
/**
* Surface with additional onLongClick functionality.
*
* @see androidx.compose.material3.Surface
*/
@Composable
@NonRestartableComposable
fun Surface(

View File

@ -1,10 +1,12 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.accompanist.swiperefresh.SwipeRefreshState
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.google.accompanist.swiperefresh.SwipeRefreshIndicator as AccompanistSwipeRefreshIndicator
@Composable
@ -21,3 +23,22 @@ fun SwipeRefreshIndicator(
refreshingOffset = refreshingOffset,
)
}
@Composable
fun SwipeRefresh(
refreshing: Boolean,
onRefresh: () -> Unit,
enabled: Boolean,
indicatorPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable () -> Unit,
) {
com.google.accompanist.swiperefresh.SwipeRefresh(
state = rememberSwipeRefreshState(refreshing),
onRefresh = onRefresh,
swipeEnabled = enabled,
indicatorPadding = indicatorPadding,
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
) {
content()
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.components
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
@ -17,6 +18,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
import kotlinx.coroutines.launch
@Composable
@ -95,7 +97,11 @@ fun TabbedScreen(
state = state,
verticalAlignment = Alignment.Top,
) { page ->
tabs[page].content()
tabs[page].content(
TachiyomiBottomNavigationView.withBottomNavPadding(
PaddingValues(bottom = contentPadding.calculateBottomPadding()),
),
)
}
}
}
@ -105,5 +111,5 @@ data class TabContent(
@StringRes val titleRes: Int,
val badgeNumber: Int? = null,
val actions: List<AppBar.Action> = emptyList(),
val content: @Composable () -> Unit,
val content: @Composable (contentPadding: PaddingValues) -> Unit,
)

View File

@ -42,9 +42,10 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMaxBy
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
@ -53,6 +54,11 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Draws vertical fast scroller to a lazy list
*
* Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
*/
@Composable
fun VerticalFastScroller(
listState: LazyListState,
@ -386,7 +392,8 @@ private fun computeScrollRange(state: LazyGridState): Int {
private fun computeScrollOffset(state: LazyListState): Int {
if (state.layoutInfo.totalItemsCount == 0) return 0
val visibleItems = state.layoutInfo.visibleItemsInfo
val startChild = visibleItems.first()
val startChild = visibleItems
.fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!!
val endChild = visibleItems.last()
val minPosition = min(startChild.index, endChild.index)
val maxPosition = max(startChild.index, endChild.index)
@ -401,13 +408,18 @@ private fun computeScrollOffset(state: LazyListState): Int {
private fun computeScrollRange(state: LazyListState): Int {
if (state.layoutInfo.totalItemsCount == 0) return 0
val visibleItems = state.layoutInfo.visibleItemsInfo
val startChild = visibleItems.first()
val startChild = visibleItems
.fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!!
val endChild = visibleItems.last()
val laidOutArea = endChild.bottom - startChild.top
val laidOutRange = abs(startChild.index - endChild.index) + 1
return (laidOutArea.toFloat() / laidOutRange * state.layoutInfo.totalItemsCount).roundToInt()
}
object Scroller {
const val STICKY_HEADER_KEY_PREFIX = "sticky:"
}
private val ThumbLength = 48.dp
private val ThumbThickness = 8.dp
private val ThumbShape = RoundedCornerShape(ThumbThickness / 2)

View File

@ -1,9 +1,11 @@
package eu.kanade.presentation.history
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.presentation.components.EmptyScreen
@ -19,6 +21,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter.Dialog
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
import kotlinx.coroutines.flow.collectLatest
import java.util.Date
@ -41,15 +44,19 @@ fun HistoryScreen(
},
) { contentPadding ->
val items by presenter.getHistory().collectAsState(initial = null)
val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding)
items.let {
if (it == null) {
LoadingScreen()
} else if (it.isEmpty()) {
EmptyScreen(textResource = R.string.information_no_recent_manga)
EmptyScreen(
textResource = R.string.information_no_recent_manga,
modifier = Modifier.padding(contentPaddingWithNavBar),
)
} else {
HistoryContent(
history = it,
contentPadding = contentPadding,
contentPadding = contentPaddingWithNavBar,
onClickCover = onClickCover,
onClickResume = onClickResume,
onClickDelete = { item -> presenter.dialog = Dialog.Delete(item) },

View File

@ -11,8 +11,6 @@ import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.history.HistoryUiModel
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DateFormat
@ -30,7 +28,7 @@ fun HistoryContent(
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
ScrollbarLazyColumn(
contentPadding = contentPadding + bottomNavPadding + topPaddingValues,
contentPadding = contentPadding,
) {
items(
items = history,

View File

@ -1,17 +1,26 @@
package eu.kanade.presentation.library
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.library.model.display
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.components.LibraryBottomActionMenu
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.library.components.LibraryContent
import eu.kanade.presentation.library.components.LibraryToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
@Composable
fun LibraryScreen(
@ -60,9 +69,26 @@ fun LibraryScreen(
)
},
) { paddingValues ->
val contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(paddingValues)
if (presenter.searchQuery.isNullOrEmpty() && presenter.isLibraryEmpty) {
val handler = LocalUriHandler.current
EmptyScreen(
textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding),
actions = listOf(
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.Default.HelpOutline,
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
),
),
)
return@Scaffold
}
LibraryContent(
state = presenter,
contentPadding = paddingValues,
contentPadding = contentPadding,
currentPage = { presenter.activeCategory },
isLibraryEmpty = presenter.isLibraryEmpty,
showPageTabs = presenter.tabVisibility,

View File

@ -6,16 +6,15 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.presentation.components.FastScrollLazyVerticalGrid
import eu.kanade.presentation.components.TextButton
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
@Composable
fun LazyLibraryGrid(
@ -27,7 +26,7 @@ fun LazyLibraryGrid(
FastScrollLazyVerticalGrid(
columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns),
modifier = modifier,
contentPadding = contentPadding + bottomNavPadding + PaddingValues(12.dp),
contentPadding = contentPadding + PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
content = content,

View File

@ -15,20 +15,15 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalUriHandler
import com.google.accompanist.pager.rememberPagerState
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.library.LibraryState
import eu.kanade.tachiyomi.R
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.ui.library.LibraryItem
import eu.kanade.tachiyomi.widget.EmptyView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -90,7 +85,7 @@ fun LibraryContent(
}
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = isRefreshing),
refreshing = isRefreshing,
onRefresh = {
val started = onRefresh(categories[currentPage()])
if (!started) return@SwipeRefresh
@ -101,26 +96,8 @@ fun LibraryContent(
isRefreshing = false
}
},
indicator = { s, trigger ->
SwipeRefreshIndicator(
state = s,
refreshTriggerDistance = trigger,
)
},
enabled = state.selectionMode.not(),
) {
if (state.searchQuery.isNullOrEmpty() && isLibraryEmpty) {
val handler = LocalUriHandler.current
EmptyScreen(
R.string.information_empty_library,
listOf(
EmptyView.Action(R.string.getting_started_guide, R.drawable.ic_help_24dp) {
handler.openUri("https://tachiyomi.org/help/guides/getting-started")
},
),
)
return@SwipeRefresh
}
LibraryPager(
state = pagerState,
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -24,14 +25,12 @@ import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.MangaCover.Square
import eu.kanade.presentation.components.TextButton
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.presentation.util.verticalPadding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibraryItem
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
@Composable
fun LibraryList(
@ -45,7 +44,7 @@ fun LibraryList(
) {
FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = bottomNavPadding + contentPadding,
contentPadding = contentPadding,
) {
item {
if (searchQuery.isNullOrEmpty().not()) {

View File

@ -48,15 +48,13 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaBottomActionMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
@ -290,16 +288,10 @@ private fun MangaScreenSmallImpl(
val topPadding = contentPadding.calculateTopPadding()
SwipeRefresh(
state = rememberSwipeRefreshState(state.isRefreshingData),
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
swipeEnabled = !chapters.any { it.selected },
enabled = chapters.none { it.selected },
indicatorPadding = contentPadding,
indicator = { s, trigger ->
SwipeRefreshIndicator(
state = s,
refreshTriggerDistance = trigger,
)
},
) {
VerticalFastScroller(
listState = chapterListState,
@ -425,21 +417,14 @@ fun MangaScreenLargeImpl(
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
SwipeRefresh(
state = rememberSwipeRefreshState(state.isRefreshingData),
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
swipeEnabled = !chapters.any { it.selected },
enabled = chapters.none { it.selected },
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection),
),
clipIndicatorToPadding = true,
indicator = { s, trigger ->
SwipeRefreshIndicator(
state = s,
refreshTriggerDistance = trigger,
)
},
) {
val chapterListState = rememberLazyListState()

View File

@ -27,7 +27,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.more.MorePresenter
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
@Composable
fun MoreScreen(
@ -43,7 +43,7 @@ fun MoreScreen(
ScrollbarLazyColumn(
modifier = Modifier.statusBarsPadding(),
contentPadding = bottomNavPadding,
contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(),
) {
item {
LogoHeader()

View File

@ -2,9 +2,9 @@ package eu.kanade.presentation.more.settings.database.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.presentation.components.TextButton
import eu.kanade.tachiyomi.R
@Composable

View File

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlipToBack
@ -24,8 +25,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.EmptyScreen
@ -33,9 +32,8 @@ import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.MangaBottomActionMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
@ -45,7 +43,7 @@ import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter.Dialog
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter.Event
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -98,13 +96,17 @@ fun UpdateScreen(
)
},
) { contentPadding ->
val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding)
when {
presenter.isLoading -> LoadingScreen()
presenter.uiModels.isEmpty() -> EmptyScreen(textResource = R.string.information_no_recent)
presenter.uiModels.isEmpty() -> EmptyScreen(
textResource = R.string.information_no_recent,
modifier = Modifier.padding(contentPaddingWithNavBar),
)
else -> {
UpdateScreenContent(
presenter = presenter,
contentPadding = contentPadding,
contentPadding = contentPaddingWithNavBar,
onUpdateLibrary = onUpdateLibrary,
onClickCover = onClickCover,
)
@ -122,15 +124,11 @@ private fun UpdateScreenContent(
) {
val context = LocalContext.current
val updatesListState = rememberLazyListState()
// During selection mode bottom nav is not visible
val contentPaddingWithNavBar = contentPadding + bottomNavPadding
val scope = rememberCoroutineScope()
var isRefreshing by remember { mutableStateOf(false) }
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = isRefreshing),
refreshing = isRefreshing,
onRefresh = {
val started = onUpdateLibrary()
if (!started) return@SwipeRefresh
@ -141,46 +139,36 @@ private fun UpdateScreenContent(
isRefreshing = false
}
},
swipeEnabled = presenter.selectionMode.not(),
indicatorPadding = contentPaddingWithNavBar,
indicator = { s, trigger ->
SwipeRefreshIndicator(
state = s,
refreshTriggerDistance = trigger,
)
},
enabled = presenter.selectionMode.not(),
indicatorPadding = contentPadding,
) {
if (presenter.uiModels.isEmpty()) {
EmptyScreen(textResource = R.string.information_no_recent)
} else {
VerticalFastScroller(
listState = updatesListState,
topContentPadding = contentPaddingWithNavBar.calculateTopPadding(),
endContentPadding = contentPaddingWithNavBar.calculateEndPadding(LocalLayoutDirection.current),
VerticalFastScroller(
listState = updatesListState,
topContentPadding = contentPadding.calculateTopPadding(),
endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = updatesListState,
contentPadding = contentPadding,
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = updatesListState,
contentPadding = contentPaddingWithNavBar,
) {
if (presenter.lastUpdated > 0L) {
updatesLastUpdatedItem(presenter.lastUpdated)
}
updatesUiItems(
uiModels = presenter.uiModels,
selectionMode = presenter.selectionMode,
onUpdateSelected = presenter::toggleSelection,
onClickCover = onClickCover,
onClickUpdate = {
val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)
context.startActivity(intent)
},
onDownloadChapter = presenter::downloadChapters,
relativeTime = presenter.relativeTime,
dateFormat = presenter.dateFormat,
)
if (presenter.lastUpdated > 0L) {
updatesLastUpdatedItem(presenter.lastUpdated)
}
updatesUiItems(
uiModels = presenter.uiModels,
selectionMode = presenter.selectionMode,
onUpdateSelected = presenter::toggleSelection,
onClickCover = onClickCover,
onClickUpdate = {
val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)
context.startActivity(intent)
},
onDownloadChapter = presenter::downloadChapters,
relativeTime = presenter.relativeTime,
dateFormat = presenter.dateFormat,
)
}
}
}

View File

@ -62,11 +62,18 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastSumBy
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
/**
* Draws horizontal scrollbar to a LazyList.
*
* Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
*/
fun Modifier.drawHorizontalScrollbar(
state: LazyListState,
reverseScrolling: Boolean = false,
@ -74,6 +81,11 @@ fun Modifier.drawHorizontalScrollbar(
positionOffsetPx: Float = 0f,
): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling, positionOffsetPx)
/**
* Draws vertical scrollbar to a LazyList.
*
* Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
*/
fun Modifier.drawVerticalScrollbar(
state: LazyListState,
reverseScrolling: Boolean = false,
@ -106,7 +118,7 @@ private fun Modifier.drawScrollbar(
0f
} else {
items
.first()
.fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!!
.run {
val startPadding = if (reverseDirection) layoutInfo.afterContentPadding else layoutInfo.beforeContentPadding
startPadding + ((estimatedItemSize * index - offset) / totalSize * viewportSize)

View File

@ -8,11 +8,9 @@ import android.os.IBinder
import android.os.PowerManager
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
@ -32,7 +30,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
import ru.beryukhov.reactivenetwork.ReactiveNetwork
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
/**
@ -44,11 +41,6 @@ class DownloadService : Service() {
companion object {
/**
* Relay used to know when the service is running.
*/
val runningRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
private val _isRunning = MutableStateFlow(false)
val isRunning = _isRunning.asStateFlow()
@ -83,7 +75,6 @@ class DownloadService : Service() {
}
private val downloadManager: DownloadManager by injectLazy()
private val downloadPreferences: DownloadPreferences by injectLazy()
/**
@ -91,62 +82,55 @@ class DownloadService : Service() {
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var subscriptions: CompositeSubscription
private lateinit var ioScope: CoroutineScope
/**
* Called when the service is created.
*/
override fun onCreate() {
super.onCreate()
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
wakeLock = acquireWakeLock(javaClass.name)
runningRelay.call(true)
_isRunning.value = true
subscriptions = CompositeSubscription()
listenDownloaderState()
listenNetworkChanges()
}
/**
* Called when the service is destroyed.
*/
override fun onDestroy() {
ioScope?.cancel()
runningRelay.call(false)
_isRunning.value = false
subscriptions.unsubscribe()
downloadManager.stopDownloads()
wakeLock.releaseIfNeeded()
wakeLock.releaseIfHeld()
super.onDestroy()
}
/**
* Not used.
*/
// Not used
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_NOT_STICKY
}
/**
* Not used.
*/
// Not used
override fun onBind(intent: Intent): IBinder? {
return null
}
/**
* Listens to network changes.
*
* @see onNetworkStateChanged
*/
private fun stopDownloads(@StringRes string: Int) {
downloadManager.stopDownloads(getString(string))
}
private fun listenNetworkChanges() {
ReactiveNetwork()
.observeNetworkConnectivity(applicationContext)
.onEach {
withUIContext {
onNetworkStateChanged()
if (isOnline()) {
if (downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi()) {
stopDownloads(R.string.download_notifier_text_only_wifi)
} else {
val started = downloadManager.startDownloads()
if (!started) stopSelf()
}
} else {
stopDownloads(R.string.download_notifier_no_network)
}
}
}
.catch { error ->
@ -159,54 +143,29 @@ class DownloadService : Service() {
.launchIn(ioScope)
}
/**
* Called when the network state changes.
*/
private fun onNetworkStateChanged() {
if (isOnline()) {
if (downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi()) {
stopDownloads(R.string.download_notifier_text_only_wifi)
} else {
val started = downloadManager.startDownloads()
if (!started) stopSelf()
}
} else {
stopDownloads(R.string.download_notifier_no_network)
}
}
private fun stopDownloads(@StringRes string: Int) {
downloadManager.stopDownloads(getString(string))
}
/**
* Listens to downloader status. Enables or disables the wake lock depending on the status.
*/
private fun listenDownloaderState() {
subscriptions += downloadManager.runningRelay
.doOnError {
/* Swallow wakelock error */
}
.subscribe { running ->
if (running) {
wakeLock.acquireIfNeeded()
_isRunning
.onEach { isRunning ->
if (isRunning) {
wakeLock.acquireIfNotHeld()
} else {
wakeLock.releaseIfNeeded()
wakeLock.releaseIfHeld()
}
}
.catch {
// Ignore errors
}
.launchIn(ioScope)
}
/**
* Releases the wake lock if it's held.
*/
private fun PowerManager.WakeLock.releaseIfNeeded() {
private fun PowerManager.WakeLock.releaseIfHeld() {
if (isHeld) release()
}
/**
* Acquires the wake lock if it's not held.
*/
private fun PowerManager.WakeLock.acquireIfNeeded() {
private fun PowerManager.WakeLock.acquireIfNotHeld() {
if (!isHeld) acquire()
}

View File

@ -75,13 +75,14 @@ class DownloadQueue(
Observable.from(this).filter { download -> download.status == Download.State.DOWNLOADING }
@Deprecated("Use getStatusAsFlow instead")
fun getStatusObservable(): Observable<Download> = statusSubject
private fun getStatusObservable(): Observable<Download> = statusSubject
.startWith(getActiveDownloads())
.onBackpressureBuffer()
fun getStatusAsFlow(): Flow<Download> = getStatusObservable().asFlow()
fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
@Deprecated("Use getUpdatedAsFlow instead")
private fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
.startWith(Unit)
.map { this }
@ -94,7 +95,7 @@ class DownloadQueue(
}
@Deprecated("Use getProgressAsFlow instead")
fun getProgressObservable(): Observable<Download> {
private fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads())
.flatMap { download ->
@ -113,9 +114,7 @@ class DownloadQueue(
.filter { it.status == Download.State.DOWNLOADING }
}
fun getProgressAsFlow(): Flow<Download> {
return getProgressObservable().asFlow()
}
fun getProgressAsFlow(): Flow<Download> = getProgressObservable().asFlow()
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
pages?.forEach { it.setStatusSubject(subject) }

View File

@ -34,9 +34,10 @@ fun extensionsTab(
onClick = { router?.pushController(ExtensionFilterController()) },
),
),
content = {
content = { contentPadding ->
ExtensionScreen(
presenter = presenter,
contentPadding = contentPadding,
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> presenter.installExtension(extension)

View File

@ -31,9 +31,10 @@ fun migrateSourcesTab(
},
),
),
content = {
content = { contentPadding ->
MigrateSourceScreen(
presenter = presenter,
contentPadding = contentPadding,
onClickItem = { source ->
router?.pushController(
MigrationMangaController(

View File

@ -32,9 +32,10 @@ fun sourcesTab(
onClick = { router?.pushController(SourceFilterController()) },
),
),
content = {
content = { contentPadding ->
SourcesScreen(
presenter = presenter,
contentPadding = contentPadding,
onClickItem = { source, query ->
presenter.onOpenSource(source)
router?.pushController(BrowseSourceController(source, query))

View File

@ -91,6 +91,7 @@ open class BrowseSourceController(bundle: Bundle) :
onConfirm = {
presenter.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is Dialog.ChangeMangaCategory -> {

View File

@ -256,7 +256,10 @@ class DownloadController :
},
) { contentPadding ->
if (downloadList.isEmpty()) {
EmptyScreen(textResource = R.string.information_no_downloads)
EmptyScreen(
textResource = R.string.information_no_downloads,
modifier = Modifier.padding(contentPadding),
)
return@Scaffold
}
val density = LocalDensity.current

View File

@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

View File

@ -46,7 +46,6 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.combineLatest
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.removeCovers
@ -717,6 +716,10 @@ class LibraryPresenter(
state.selection = items.filterNot { it in selection }
}
private fun <T, U, R> Observable<T>.combineLatest(o2: Observable<U>, combineFn: (T, U) -> R): Observable<R> {
return Observable.combineLatest(this, o2, combineFn)
}
sealed class Dialog {
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
data class DeleteManga(val manga: List<Manga>) : Dialog()

View File

@ -9,17 +9,14 @@ import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MorePresenter(
private val downloadManager: DownloadManager = Injekt.get(),
preferences: BasePreferences = Injekt.get(),
) : BasePresenter<MoreController>() {
val downloadedOnly = preferences.downloadedOnly().asState()
@ -28,58 +25,26 @@ class MorePresenter(
private var _state: MutableStateFlow<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped)
val downloadQueueState: StateFlow<DownloadQueueState> = _state.asStateFlow()
private var isDownloading: Boolean = false
private var downloadQueueSize: Int = 0
private var untilDestroySubscriptions = CompositeSubscription()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}
initDownloadQueueSummary()
}
override fun onDestroy() {
super.onDestroy()
untilDestroySubscriptions.unsubscribe()
}
private fun initDownloadQueueSummary() {
// Handle running/paused status change
DownloadService.runningRelay
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { isRunning ->
isDownloading = isRunning
updateDownloadQueueState()
}
// Handle queue progress updating
downloadManager.queue.getUpdatedObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy {
downloadQueueSize = it.size
updateDownloadQueueState()
}
}
private fun updateDownloadQueueState() {
// Handle running/paused status change and queue progress updating
presenterScope.launchIO {
val pendingDownloadExists = downloadQueueSize != 0
_state.value = when {
!pendingDownloadExists -> DownloadQueueState.Stopped
!isDownloading && !pendingDownloadExists -> DownloadQueueState.Paused(0)
!isDownloading && pendingDownloadExists -> DownloadQueueState.Paused(downloadQueueSize)
else -> DownloadQueueState.Downloading(downloadQueueSize)
}
combine(
DownloadService.isRunning,
downloadManager.queue.getUpdatedAsFlow(),
) { isRunning, downloadQueue -> Pair(isRunning, downloadQueue.size) }
.collectLatest { (isDownloading, downloadQueueSize) ->
val pendingDownloadExists = downloadQueueSize != 0
_state.value = when {
!pendingDownloadExists -> DownloadQueueState.Stopped
!isDownloading && !pendingDownloadExists -> DownloadQueueState.Paused(0)
!isDownloading && pendingDownloadExists -> DownloadQueueState.Paused(downloadQueueSize)
else -> DownloadQueueState.Downloading(downloadQueueSize)
}
}
}
}
private fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
}
}
sealed class DownloadQueueState {

View File

@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import logcat.LogPriority

View File

@ -1,11 +1,6 @@
package eu.kanade.tachiyomi.util.lang
import rx.Observable
import rx.Subscription
import rx.subscriptions.CompositeSubscription
operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(subscription)
fun <T, U, R> Observable<T>.combineLatest(o2: Observable<U>, combineFn: (T, U) -> R): Observable<R> {
return Observable.combineLatest(this, o2, combineFn)
}

View File

@ -1,26 +1,24 @@
package eu.kanade.tachiyomi.widget
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.AbstractComposeView
import androidx.core.view.isVisible
import com.google.android.material.button.MaterialButton
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.CommonViewEmptyBinding
import eu.kanade.tachiyomi.util.system.getThemeColor
import kotlin.random.Random
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.theme.TachiyomiTheme
class EmptyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
RelativeLayout(context, attrs) {
AbstractComposeView(context, attrs) {
private val binding: CommonViewEmptyBinding =
CommonViewEmptyBinding.inflate(LayoutInflater.from(context), this, true)
var message by mutableStateOf("")
/**
* Hide the information view
@ -33,62 +31,17 @@ class EmptyView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
* Show the information view
* @param textResource text of information view
*/
fun show(@StringRes textResource: Int, actions: List<Action>? = null) {
show(context.getString(textResource), actions)
}
fun show(message: String, actions: List<Action>? = null) {
binding.textFace.text = getRandomErrorFace()
binding.textLabel.text = message
binding.actionsContainer.removeAllViews()
val buttonContext = ContextThemeWrapper(context, R.style.Widget_Tachiyomi_Button_ActionButton)
val buttonColor = ColorStateList.valueOf(context.getThemeColor(R.attr.colorOnBackground))
actions?.forEach {
val button = MaterialButton(
buttonContext,
null,
R.attr.borderlessButtonStyle,
).apply {
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f / actions.size,
)
setTextColor(buttonColor)
iconTint = buttonColor
setIconResource(it.iconResId)
setText(it.stringResId)
setOnClickListener(it.listener)
}
binding.actionsContainer.addView(button)
}
fun show(@StringRes textResource: Int) {
message = context.getString(textResource)
this.isVisible = true
}
companion object {
private val ERROR_FACES = listOf(
"(・o・;)",
"Σ(ಠ_ಠ)",
"ಥ_ಥ",
"(˘・_・˘)",
"(; ̄Д ̄)",
"(・Д・。",
)
fun getRandomErrorFace(): String {
return ERROR_FACES[Random.nextInt(ERROR_FACES.size)]
@Composable
override fun Content() {
TachiyomiTheme {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
EmptyScreen(message = message)
}
}
}
data class Action(
@StringRes val stringResId: Int,
@DrawableRes val iconResId: Int,
val listener: OnClickListener,
)
}

View File

@ -9,10 +9,16 @@ import android.os.Parcelable
import android.util.AttributeSet
import android.view.ViewPropertyAnimator
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.customview.view.AbsSavedState
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
@ -58,7 +64,7 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
bottomNavPadding = PaddingValues(bottom = h.pxToDp.dp)
bottomNavPadding = h.pxToDp.dp
}
/**
@ -74,6 +80,7 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
SLIDE_UP_ANIMATION_DURATION,
LinearOutSlowInInterpolator(),
)
bottomNavPadding = height.pxToDp.dp
}
/**
@ -89,6 +96,7 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
SLIDE_DOWN_ANIMATION_DURATION,
FastOutLinearInInterpolator(),
)
bottomNavPadding = 0.dp
}
private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
@ -149,7 +157,21 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
private const val SLIDE_UP_ANIMATION_DURATION = 225L
private const val SLIDE_DOWN_ANIMATION_DURATION = 175L
var bottomNavPadding by mutableStateOf(PaddingValues())
private set
private var bottomNavPadding by mutableStateOf(0.dp)
/**
* Merges [bottomNavPadding] to the origin's [PaddingValues] bottom side.
*/
@ReadOnlyComposable
@Composable
fun withBottomNavPadding(origin: PaddingValues = PaddingValues()): PaddingValues {
val layoutDirection = LocalLayoutDirection.current
return PaddingValues(
start = origin.calculateStartPadding(layoutDirection),
top = origin.calculateTopPadding(),
end = origin.calculateEndPadding(layoutDirection),
bottom = max(origin.calculateBottomPadding(), bottomNavPadding),
)
}
}
}

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/text_face"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
android:textSize="48sp"
tools:text="-_-" />
<TextView
android:id="@+id/text_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
tools:text="Label" />
<LinearLayout
android:id="@+id/actions_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</LinearLayout>

View File

@ -81,16 +81,6 @@
</style>
<!--==============-->
<!--Widgets.Button-->
<!--==============-->
<style name="Widget.Tachiyomi.Button.ActionButton" parent="Widget.Material3.Button.TextButton.Icon">
<item name="iconGravity">top</item>
<item name="iconTint">@color/button_action_selector</item>
<item name="iconPadding">4dp</item>
<item name="android:textColor">@color/button_action_selector</item>
<item name="android:textSize">12sp</item>
</style>
<!--=======================-->
<!--Widgets.MaterialDivider-->
<!--=======================-->

View File

@ -2,7 +2,7 @@
compiler = "1.3.1"
compose = "1.2.1"
accompanist = "0.25.1"
material3 = "1.0.0-beta03"
material3 = "1.0.0-rc01"
[libraries]
activity = "androidx.activity:activity-compose:1.6.0"
@ -14,7 +14,7 @@ ui-util = { module = "androidx.compose.ui:ui-util", version.ref = "compose" }
material3-core = { module = "androidx.compose.material3:material3", version.ref = "material3" }
material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3" }
material3-adapter = "com.google.android.material:compose-theme-adapter-3:1.0.19"
material3-adapter = "com.google.android.material:compose-theme-adapter-3:1.0.20"
material-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }

View File

@ -5,7 +5,7 @@ nucleus_version = "3.0.0"
coil_version = "2.2.2"
conductor_version = "3.1.7"
flowbinding_version = "1.2.0"
shizuku_version = "12.1.0"
shizuku_version = "12.2.0"
sqldelight = "1.5.3"
leakcanary = "2.9.1"
@ -33,7 +33,7 @@ disklrucache = "com.jakewharton:disklrucache:2.0.2"
unifile = "com.github.tachiyomiorg:unifile:17bec43"
junrar = "com.github.junrar:junrar:7.5.3"
sqlitektx = "androidx.sqlite:sqlite-ktx:2.3.0-alpha05"
sqlitektx = "androidx.sqlite:sqlite-ktx:2.3.0-beta01"
sqlite-android = "com.github.requery:sqlite-android:3.36.0"
preferencektx = "androidx.preference:preference-ktx:1.2.0"

View File

@ -876,5 +876,5 @@
<!-- App widget -->
<string name="appwidget_updates_description">See your recently updated manga</string>
<string name="appwidget_unavailable_locked">Widget not available when app lock is enabled</string>
<string name="remove_manga">You are about to remove this manga from your library</string>
<string name="remove_manga">You are about to remove \"%s\" from your library</string>
</resources>