Merge branch 'tachiyomiorg:master' into range_selection
This commit is contained in:
commit
f0bc8a97ba
@ -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)
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)]
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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. They’re used when a
|
||||
* compact button is required, such as in a toolbar or image list.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* [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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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) },
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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()),
|
||||
|
@ -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()) {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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) }
|
||||
|
@ -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)
|
||||
|
@ -31,9 +31,10 @@ fun migrateSourcesTab(
|
||||
},
|
||||
),
|
||||
),
|
||||
content = {
|
||||
content = { contentPadding ->
|
||||
MigrateSourceScreen(
|
||||
presenter = presenter,
|
||||
contentPadding = contentPadding,
|
||||
onClickItem = { source ->
|
||||
router?.pushController(
|
||||
MigrationMangaController(
|
||||
|
@ -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))
|
||||
|
@ -91,6 +91,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
onConfirm = {
|
||||
presenter.changeMangaFavorite(dialog.manga)
|
||||
},
|
||||
mangaToRemove = dialog.manga,
|
||||
)
|
||||
}
|
||||
is Dialog.ChangeMangaCategory -> {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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-->
|
||||
<!--=======================-->
|
||||
|
@ -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" }
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user