From 300660492271752a3f75da4f158bd9b8e049a45b Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:08:41 +0600 Subject: [PATCH 1/4] MangaScreen: Fix close in action mode exists from screen (#10160) * MangaScreen: Fix close in action mode exists from screen * L --- .../kanade/presentation/manga/MangaScreen.kt | 26 +++++++++---------- .../tachiyomi/ui/manga/MangaScreenModel.kt | 5 ++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index f5009e962..c8db0c304 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -266,13 +266,12 @@ private fun MangaScreenSmallImpl( ) { val chapterListState = rememberLazyListState() - val chapters = remember(state) { state.processedChapters } - val listItem = remember(state) { state.chapterListItems } - - val isAnySelected by remember { - derivedStateOf { - chapters.fastAny { it.selected } - } + val (chapters, listItem, isAnySelected) = remember(state) { + Triple( + first = state.processedChapters, + second = state.chapterListItems, + third = state.isAnySelected, + ) } val internalOnBackPressed = { @@ -520,13 +519,12 @@ fun MangaScreenLargeImpl( val layoutDirection = LocalLayoutDirection.current val density = LocalDensity.current - val chapters = remember(state) { state.processedChapters } - val listItem = remember(state) { state.chapterListItems } - - val isAnySelected by remember { - derivedStateOf { - chapters.fastAny { it.selected } - } + val (chapters, listItem, isAnySelected) = remember(state) { + Triple( + first = state.processedChapters, + second = state.chapterListItems, + third = state.isAnySelected, + ) } val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index e49394b0f..73c64e12b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.util.fastAny import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.preference.asState @@ -1052,6 +1053,10 @@ class MangaScreenModel( chapters.applyFilters(manga).toList() } + val isAnySelected by lazy { + chapters.fastAny { it.selected } + } + val chapterListItems by lazy { processedChapters.insertSeparators { before, after -> val (lowerChapter, higherChapter) = if (manga.sortDescending()) { From d59cb9c1e3119b300fb4f0568ac5c77e5addaf47 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:09:41 +0700 Subject: [PATCH 2/4] Migrate to M3 pull-to-refresh (#10164) --- .../presentation/browse/ExtensionsScreen.kt | 2 +- .../presentation/components/TabbedScreen.kt | 2 + .../library/components/LibraryContent.kt | 2 +- .../library/components/LibraryTabs.kt | 6 +- .../kanade/presentation/manga/MangaScreen.kt | 177 ++++++------ .../presentation/updates/UpdatesScreen.kt | 2 +- .../core/components/material/PullRefresh.kt | 273 ++++++++++++++++-- 7 files changed, 344 insertions(+), 120 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index c914d8e81..3469af6ca 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -73,7 +73,7 @@ fun ExtensionScreen( PullRefresh( refreshing = state.isRefreshing, onRefresh = onRefresh, - enabled = !state.isLoading, + enabled = { !state.isLoading }, ) { when { state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index e7f185b71..81ad61f2a 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.zIndex import dev.icerock.moko.resources.StringResource import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -70,6 +71,7 @@ fun TabbedScreen( ) { PrimaryTabRow( selectedTabIndex = state.currentPage, + modifier = Modifier.zIndex(1f), ) { tabs.forEachIndexed { index, tab -> Tab( diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt index 61da10345..f0a63f597 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt @@ -93,7 +93,7 @@ fun LibraryContent( isRefreshing = false } }, - enabled = notSelectionMode, + enabled = { notSelectionMode }, ) { LibraryPager( state = pagerState, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt index 3200d437d..1904d39c2 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt @@ -7,7 +7,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import eu.kanade.presentation.category.visualName import tachiyomi.domain.category.model.Category import tachiyomi.presentation.core.components.material.TabText @@ -19,7 +21,9 @@ internal fun LibraryTabs( getNumberOfMangaForCategory: (Category) -> Int?, onTabItemClick: (Int) -> Unit, ) { - Column { + Column( + modifier = Modifier.zIndex(1f), + ) { PrimaryScrollableTabRow( selectedTabIndex = pagerState.currentPage, edgePadding = 0.dp, diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index c8db0c304..dda12b12b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -364,8 +364,8 @@ private fun MangaScreenSmallImpl( PullRefresh( refreshing = state.isRefreshingData, onRefresh = onRefresh, - enabled = !isAnySelected, - indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(), + enabled = { !isAnySelected }, + indicatorPadding = PaddingValues(top = topPadding), ) { val layoutDirection = LocalLayoutDirection.current VerticalFastScroller( @@ -529,97 +529,98 @@ fun MangaScreenLargeImpl( val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() var topBarHeight by remember { mutableIntStateOf(0) } - PullRefresh( - refreshing = state.isRefreshingData, - onRefresh = onRefresh, - enabled = !isAnySelected, - indicatorPadding = PaddingValues( - start = insetPadding.calculateStartPadding(layoutDirection), - top = with(density) { topBarHeight.toDp() }, - end = insetPadding.calculateEndPadding(layoutDirection), - ), - ) { - val chapterListState = rememberLazyListState() - val internalOnBackPressed = { - if (isAnySelected) { - onAllChapterSelected(false) - } else { - onBackClicked() - } + val chapterListState = rememberLazyListState() + + val internalOnBackPressed = { + if (isAnySelected) { + onAllChapterSelected(false) + } else { + onBackClicked() } - BackHandler(onBack = internalOnBackPressed) + } + BackHandler(onBack = internalOnBackPressed) - Scaffold( - topBar = { - val selectedChapterCount = remember(chapters) { - chapters.count { it.selected } + Scaffold( + topBar = { + val selectedChapterCount = remember(chapters) { + chapters.count { it.selected } + } + MangaToolbar( + modifier = Modifier.onSizeChanged { topBarHeight = it.height }, + title = state.manga.title, + titleAlphaProvider = { if (isAnySelected) 1f else 0f }, + backgroundAlphaProvider = { 1f }, + hasFilters = state.filterActive, + onBackClicked = internalOnBackPressed, + onClickFilter = onFilterButtonClicked, + onClickShare = onShareClicked, + onClickDownload = onDownloadActionClicked, + onClickEditCategory = onEditCategoryClicked, + onClickRefresh = onRefresh, + onClickMigrate = onMigrateClicked, + actionModeCounter = selectedChapterCount, + onSelectAll = { onAllChapterSelected(true) }, + onInvertSelection = { onInvertSelection() }, + ) + }, + bottomBar = { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.BottomEnd, + ) { + val selectedChapters = remember(chapters) { + chapters.filter { it.selected } } - MangaToolbar( - modifier = Modifier.onSizeChanged { topBarHeight = it.height }, - title = state.manga.title, - titleAlphaProvider = { if (isAnySelected) 1f else 0f }, - backgroundAlphaProvider = { 1f }, - hasFilters = state.filterActive, - onBackClicked = internalOnBackPressed, - onClickFilter = onFilterButtonClicked, - onClickShare = onShareClicked, - onClickDownload = onDownloadActionClicked, - onClickEditCategory = onEditCategoryClicked, - onClickRefresh = onRefresh, - onClickMigrate = onMigrateClicked, - actionModeCounter = selectedChapterCount, - onSelectAll = { onAllChapterSelected(true) }, - onInvertSelection = { onInvertSelection() }, + SharedMangaBottomActionMenu( + selected = selectedChapters, + onMultiBookmarkClicked = onMultiBookmarkClicked, + onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, + onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, + onDownloadChapter = onDownloadChapter, + onMultiDeleteClicked = onMultiDeleteClicked, + fillFraction = 0.5f, ) - }, - bottomBar = { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.BottomEnd, - ) { - val selectedChapters = remember(chapters) { - chapters.filter { it.selected } - } - SharedMangaBottomActionMenu( - selected = selectedChapters, - onMultiBookmarkClicked = onMultiBookmarkClicked, - onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, - onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, - onDownloadChapter = onDownloadChapter, - onMultiDeleteClicked = onMultiDeleteClicked, - fillFraction = 0.5f, - ) - } - }, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - floatingActionButton = { - val isFABVisible = remember(chapters) { - chapters.fastAny { !it.chapter.read } && !isAnySelected - } - AnimatedVisibility( - visible = isFABVisible, - enter = fadeIn(), - exit = fadeOut(), - ) { - ExtendedFloatingActionButton( - text = { - val isReading = remember(state.chapters) { - state.chapters.fastAny { it.chapter.read } - } - Text( - text = stringResource( - if (isReading) MR.strings.action_resume else MR.strings.action_start, - ), - ) - }, - icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, - onClick = onContinueReading, - expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), - ) - } - }, - ) { contentPadding -> + } + }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + floatingActionButton = { + val isFABVisible = remember(chapters) { + chapters.fastAny { !it.chapter.read } && !isAnySelected + } + AnimatedVisibility( + visible = isFABVisible, + enter = fadeIn(), + exit = fadeOut(), + ) { + ExtendedFloatingActionButton( + text = { + val isReading = remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } + } + Text( + text = stringResource( + if (isReading) MR.strings.action_resume else MR.strings.action_start, + ), + ) + }, + icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, + onClick = onContinueReading, + expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), + ) + } + }, + ) { contentPadding -> + PullRefresh( + refreshing = state.isRefreshingData, + onRefresh = onRefresh, + enabled = { !isAnySelected }, + indicatorPadding = PaddingValues( + start = insetPadding.calculateStartPadding(layoutDirection), + top = with(density) { topBarHeight.toDp() }, + end = insetPadding.calculateEndPadding(layoutDirection), + ), + ) { TwoPanelBox( modifier = Modifier.padding( start = contentPadding.calculateStartPadding(layoutDirection), diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index 15f1f57cb..3fd9cd0a4 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -104,7 +104,7 @@ fun UpdateScreen( isRefreshing = false } }, - enabled = !state.selectionMode, + enabled = { !state.selectionMode }, indicatorPadding = contentPadding, ) { FastScrollLazyColumn( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt index 2200b5354..c68dd300f 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt @@ -1,17 +1,33 @@ package tachiyomi.presentation.core.components.material +import androidx.compose.animation.core.animate import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp +import kotlin.math.abs +import kotlin.math.pow /** * @param refreshing Whether the layout is currently refreshing @@ -19,38 +35,239 @@ import androidx.compose.ui.unit.dp * @param enabled Whether the the layout should react to swipe gestures or not. * @param indicatorPadding Content padding for the indicator, to inset the indicator in if required. * @param content The content containing a vertically scrollable composable. - * - * Code reference: [Accompanist SwipeRefresh](https://github.com/google/accompanist/blob/677bc4ca0ee74677a8ba73793d04d85fe4ab55fb/swiperefresh/src/main/java/com/google/accompanist/swiperefresh/SwipeRefresh.kt#L265-L283) */ @Composable fun PullRefresh( refreshing: Boolean, + enabled: () -> Boolean, onRefresh: () -> Unit, - enabled: Boolean, + modifier: Modifier = Modifier, indicatorPadding: PaddingValues = PaddingValues(0.dp), content: @Composable () -> Unit, ) { - val state = rememberPullRefreshState( - refreshing = refreshing, - onRefresh = onRefresh, + val state = rememberPullToRefreshState( + extraVerticalOffset = indicatorPadding.calculateTopPadding(), + enabled = enabled, ) - - Box(Modifier.pullRefresh(state, enabled)) { - content() - - Box( - Modifier - .padding(indicatorPadding) - .matchParentSize() - .clipToBounds(), - ) { - PullRefreshIndicator( - refreshing = refreshing, - state = state, - modifier = Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ) + if (state.isRefreshing) { + LaunchedEffect(true) { + onRefresh() } } + LaunchedEffect(refreshing) { + if (refreshing && !state.isRefreshing) { + state.startRefreshAnimated() + } else if (!refreshing && state.isRefreshing) { + state.endRefreshAnimated() + } + } + + Box(modifier.nestedScroll(state.nestedScrollConnection)) { + content() + + val contentPadding = remember(indicatorPadding) { + object : PaddingValues { + override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = + indicatorPadding.calculateLeftPadding(layoutDirection) + + override fun calculateTopPadding(): Dp = 0.dp + + override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = + indicatorPadding.calculateRightPadding(layoutDirection) + + override fun calculateBottomPadding(): Dp = + indicatorPadding.calculateBottomPadding() + } + } + PullToRefreshContainer( + state = state, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(contentPadding), + ) + } +} + +@Composable +private fun rememberPullToRefreshState( + extraVerticalOffset: Dp, + positionalThreshold: Dp = 64.dp, + enabled: () -> Boolean = { true }, +): PullToRefreshStateImpl { + val density = LocalDensity.current + val extraVerticalOffsetPx = with(density) { extraVerticalOffset.toPx() } + val positionalThresholdPx = with(density) { positionalThreshold.toPx() } + return rememberSaveable( + extraVerticalOffset, + positionalThresholdPx, + enabled, + saver = PullToRefreshStateImpl.Saver( + extraVerticalOffset = extraVerticalOffsetPx, + positionalThreshold = positionalThresholdPx, + enabled = enabled, + ), + ) { + PullToRefreshStateImpl( + initialRefreshing = false, + extraVerticalOffset = extraVerticalOffsetPx, + positionalThreshold = positionalThresholdPx, + enabled = enabled, + ) + } +} + +/** + * Creates a [PullToRefreshState]. + * + * @param positionalThreshold The positional threshold, in pixels, in which a refresh is triggered + * @param extraVerticalOffset Extra vertical offset, in pixels, for the "refreshing" state + * @param initialRefreshing The initial refreshing value of [PullToRefreshState] + * @param enabled a callback used to determine whether scroll events are to be handled by this + * [PullToRefreshState] + */ +private class PullToRefreshStateImpl( + initialRefreshing: Boolean, + private val extraVerticalOffset: Float, + override val positionalThreshold: Float, + enabled: () -> Boolean, +) : PullToRefreshState { + + override val progress get() = adjustedDistancePulled / positionalThreshold + override var verticalOffset by mutableFloatStateOf(0f) + + override var isRefreshing by mutableStateOf(initialRefreshing) + + override fun startRefresh() { + isRefreshing = true + verticalOffset = positionalThreshold + extraVerticalOffset + } + + suspend fun startRefreshAnimated() { + isRefreshing = true + animateTo(positionalThreshold + extraVerticalOffset) + } + + override fun endRefresh() { + verticalOffset = 0f + isRefreshing = false + } + + suspend fun endRefreshAnimated() { + animateTo(0f) + isRefreshing = false + } + + override var nestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset = when { + !enabled() -> Offset.Zero + // Swiping up + source == NestedScrollSource.Drag && available.y < 0 -> { + consumeAvailableOffset(available) + } + else -> Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset = when { + !enabled() -> Offset.Zero + // Swiping down + source == NestedScrollSource.Drag && available.y > 0 -> { + consumeAvailableOffset(available) + } + else -> Offset.Zero + } + + override suspend fun onPreFling(available: Velocity): Velocity { + return Velocity(0f, onRelease(available.y)) + } + } + + /** Helper method for nested scroll connection */ + fun consumeAvailableOffset(available: Offset): Offset { + val y = if (isRefreshing) { + 0f + } else { + val newOffset = (distancePulled + available.y).coerceAtLeast(0f) + val dragConsumed = newOffset - distancePulled + distancePulled = newOffset + verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress) + dragConsumed + } + return Offset(0f, y) + } + + /** Helper method for nested scroll connection. Calls onRefresh callback when triggered */ + suspend fun onRelease(velocity: Float): Float { + if (isRefreshing) return 0f // Already refreshing, do nothing + // Trigger refresh + if (adjustedDistancePulled > positionalThreshold) { + startRefreshAnimated() + } else { + animateTo(0f) + } + + val consumed = when { + // We are flinging without having dragged the pull refresh (for example a fling inside + // a list) - don't consume + distancePulled == 0f -> 0f + // If the velocity is negative, the fling is upwards, and we don't want to prevent the + // the list from scrolling + velocity < 0f -> 0f + // We are showing the indicator, and the fling is downwards - consume everything + else -> velocity + } + distancePulled = 0f + return consumed + } + + suspend fun animateTo(offset: Float) { + animate(initialValue = verticalOffset, targetValue = offset) { value, _ -> + verticalOffset = value + } + } + + /** Provides custom vertical offset behavior for [PullToRefreshContainer] */ + fun calculateVerticalOffset(): Float = when { + // If drag hasn't gone past the threshold, the position is the adjustedDistancePulled. + adjustedDistancePulled <= positionalThreshold -> adjustedDistancePulled + else -> { + // How far beyond the threshold pull has gone, as a percentage of the threshold. + val overshootPercent = abs(progress) - 1.0f + // Limit the overshoot to 200%. Linear between 0 and 200. + val linearTension = overshootPercent.coerceIn(0f, 2f) + // Non-linear tension. Increases with linearTension, but at a decreasing rate. + val tensionPercent = linearTension - linearTension.pow(2) / 4 + // The additional offset beyond the threshold. + val extraOffset = positionalThreshold * tensionPercent + positionalThreshold + extraOffset + } + } + + companion object { + /** The default [Saver] for [PullToRefreshStateImpl]. */ + fun Saver( + extraVerticalOffset: Float, + positionalThreshold: Float, + enabled: () -> Boolean, + ) = Saver( + save = { it.isRefreshing }, + restore = { isRefreshing -> + PullToRefreshStateImpl( + initialRefreshing = isRefreshing, + extraVerticalOffset = extraVerticalOffset, + positionalThreshold = positionalThreshold, + enabled = enabled, + ) + }, + ) + } + + private var distancePulled by mutableFloatStateOf(0f) + private val adjustedDistancePulled: Float get() = distancePulled * 0.5f } From bcc42dd259cd528641ae4963b9b6ef290332bb27 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 21 Nov 2023 22:11:44 -0500 Subject: [PATCH 3/4] Exclude some more app state preferences from backups --- app/build.gradle.kts | 2 +- app/src/main/java/eu/kanade/tachiyomi/Migrations.kt | 7 +++++-- .../kanade/tachiyomi/extension/api/ExtensionGithubApi.kt | 2 +- .../domain/release/interactor/GetApplicationRelease.kt | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 135931545..4bba73d1d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 109 + versionCode = 110 versionName = "0.14.7" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index f38dd415c..c55ce35da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -50,7 +50,7 @@ object Migrations { backupPreferences: BackupPreferences, trackerManager: TrackerManager, ): Boolean { - val lastVersionCode = preferenceStore.getInt("last_version_code", 0) + val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0) val oldVersion = lastVersionCode.get() if (oldVersion < BuildConfig.VERSION_CODE) { lastVersionCode.set(BuildConfig.VERSION_CODE) @@ -396,7 +396,7 @@ object Migrations { newKey = { Preference.privateKey(it) }, ) } - if (oldVersion < 108) { + if (oldVersion < 110) { val prefsToReplace = listOf( "pref_download_only", "incognito_mode", @@ -406,6 +406,9 @@ object Migrations { "library_update_last_timestamp", "library_unseen_updates_count", "last_used_category", + "last_app_check", + "last_ext_check", + "last_version_code", ) replacePreferences( preferenceStore = preferenceStore, diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index f2a1c25ed..6d34d9e52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -28,7 +28,7 @@ internal class ExtensionGithubApi { private val json: Json by injectLazy() private val lastExtCheck: Preference by lazy { - preferenceStore.getLong("last_ext_check", 0) + preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0) } private var requiresFallbackSource = false diff --git a/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt b/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt index 113e48af3..2f7709f39 100644 --- a/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt +++ b/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt @@ -13,7 +13,7 @@ class GetApplicationRelease( ) { private val lastChecked: Preference by lazy { - preferenceStore.getLong("last_app_check", 0) + preferenceStore.getLong(Preference.appStateKey("last_app_check"), 0) } suspend fun await(arguments: Arguments): Result { From 60150423d7771d9317883e1e2a26a59fba886a72 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 21 Nov 2023 22:30:32 -0500 Subject: [PATCH 4/4] Call WheelPicker onSelectionChanged with initial value Fixes #10157 We realistically only ever use the picker in contexts where we later confirm or cancel with the selected value, so this is fine. If the caller wants to ignore the initial value, they can always check if it's distinct before/after there. --- .../java/tachiyomi/presentation/core/components/WheelPicker.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt index 636e1ebac..9cbb156c6 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import tachiyomi.presentation.core.components.material.padding @@ -126,7 +125,6 @@ private fun WheelPicker( snapshotFlow { lazyListState.firstVisibleItemScrollOffset } .map { calculateSnappedItemIndex(lazyListState) } .distinctUntilChanged() - .drop(1) .collectLatest { haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) internalOnSelectionChanged(it)