Merge branch 'master' of https://github.com/tachiyomiorg/tachiyomi into sync-part-final

This commit is contained in:
KaiserBh 2023-11-22 23:14:24 +11:00
commit 2a69a1eeb0
No known key found for this signature in database
GPG Key ID: 14D73B142042BBA9
13 changed files with 369 additions and 141 deletions

View File

@ -22,7 +22,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
versionCode = 109 versionCode = 110
versionName = "0.14.7" versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")

View File

@ -73,7 +73,7 @@ fun ExtensionScreen(
PullRefresh( PullRefresh(
refreshing = state.isRefreshing, refreshing = state.isRefreshing,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = !state.isLoading, enabled = { !state.isLoading },
) { ) {
when { when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))

View File

@ -19,6 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.zIndex
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -70,6 +71,7 @@ fun TabbedScreen(
) { ) {
PrimaryTabRow( PrimaryTabRow(
selectedTabIndex = state.currentPage, selectedTabIndex = state.currentPage,
modifier = Modifier.zIndex(1f),
) { ) {
tabs.forEachIndexed { index, tab -> tabs.forEachIndexed { index, tab ->
Tab( Tab(

View File

@ -93,7 +93,7 @@ fun LibraryContent(
isRefreshing = false isRefreshing = false
} }
}, },
enabled = notSelectionMode, enabled = { notSelectionMode },
) { ) {
LibraryPager( LibraryPager(
state = pagerState, state = pagerState,

View File

@ -7,7 +7,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.components.material.TabText
@ -19,7 +21,9 @@ internal fun LibraryTabs(
getNumberOfMangaForCategory: (Category) -> Int?, getNumberOfMangaForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit, onTabItemClick: (Int) -> Unit,
) { ) {
Column { Column(
modifier = Modifier.zIndex(1f),
) {
PrimaryScrollableTabRow( PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp, edgePadding = 0.dp,

View File

@ -266,13 +266,12 @@ private fun MangaScreenSmallImpl(
) { ) {
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
val chapters = remember(state) { state.processedChapters } val (chapters, listItem, isAnySelected) = remember(state) {
val listItem = remember(state) { state.chapterListItems } Triple(
first = state.processedChapters,
val isAnySelected by remember { second = state.chapterListItems,
derivedStateOf { third = state.isAnySelected,
chapters.fastAny { it.selected } )
}
} }
val internalOnBackPressed = { val internalOnBackPressed = {
@ -365,8 +364,8 @@ private fun MangaScreenSmallImpl(
PullRefresh( PullRefresh(
refreshing = state.isRefreshingData, refreshing = state.isRefreshingData,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = !isAnySelected, enabled = { !isAnySelected },
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(), indicatorPadding = PaddingValues(top = topPadding),
) { ) {
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
VerticalFastScroller( VerticalFastScroller(
@ -520,27 +519,17 @@ fun MangaScreenLargeImpl(
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current val density = LocalDensity.current
val chapters = remember(state) { state.processedChapters } val (chapters, listItem, isAnySelected) = remember(state) {
val listItem = remember(state) { state.chapterListItems } Triple(
first = state.processedChapters,
val isAnySelected by remember { second = state.chapterListItems,
derivedStateOf { third = state.isAnySelected,
chapters.fastAny { it.selected } )
}
} }
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
var topBarHeight by remember { mutableIntStateOf(0) } 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 chapterListState = rememberLazyListState()
val internalOnBackPressed = { val internalOnBackPressed = {
@ -622,6 +611,16 @@ fun MangaScreenLargeImpl(
} }
}, },
) { contentPadding -> ) { 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( TwoPanelBox(
modifier = Modifier.padding( modifier = Modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection), start = contentPadding.calculateStartPadding(layoutDirection),

View File

@ -104,7 +104,7 @@ fun UpdateScreen(
isRefreshing = false isRefreshing = false
} }
}, },
enabled = !state.selectionMode, enabled = { !state.selectionMode },
indicatorPadding = contentPadding, indicatorPadding = contentPadding,
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(

View File

@ -50,7 +50,7 @@ object Migrations {
backupPreferences: BackupPreferences, backupPreferences: BackupPreferences,
trackerManager: TrackerManager, trackerManager: TrackerManager,
): Boolean { ): Boolean {
val lastVersionCode = preferenceStore.getInt("last_version_code", 0) val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
val oldVersion = lastVersionCode.get() val oldVersion = lastVersionCode.get()
if (oldVersion < BuildConfig.VERSION_CODE) { if (oldVersion < BuildConfig.VERSION_CODE) {
lastVersionCode.set(BuildConfig.VERSION_CODE) lastVersionCode.set(BuildConfig.VERSION_CODE)
@ -396,7 +396,7 @@ object Migrations {
newKey = { Preference.privateKey(it) }, newKey = { Preference.privateKey(it) },
) )
} }
if (oldVersion < 108) { if (oldVersion < 110) {
val prefsToReplace = listOf( val prefsToReplace = listOf(
"pref_download_only", "pref_download_only",
"incognito_mode", "incognito_mode",
@ -406,6 +406,9 @@ object Migrations {
"library_update_last_timestamp", "library_update_last_timestamp",
"library_unseen_updates_count", "library_unseen_updates_count",
"last_used_category", "last_used_category",
"last_app_check",
"last_ext_check",
"last_version_code",
) )
replacePreferences( replacePreferences(
preferenceStore = preferenceStore, preferenceStore = preferenceStore,

View File

@ -28,7 +28,7 @@ internal class ExtensionGithubApi {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val lastExtCheck: Preference<Long> by lazy { private val lastExtCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_ext_check", 0) preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
} }
private var requiresFallbackSource = false private var requiresFallbackSource = false

View File

@ -6,6 +6,7 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.util.fastAny
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
@ -1052,6 +1053,10 @@ class MangaScreenModel(
chapters.applyFilters(manga).toList() chapters.applyFilters(manga).toList()
} }
val isAnySelected by lazy {
chapters.fastAny { it.selected }
}
val chapterListItems by lazy { val chapterListItems by lazy {
processedChapters.insertSeparators { before, after -> processedChapters.insertSeparators { before, after ->
val (lowerChapter, higherChapter) = if (manga.sortDescending()) { val (lowerChapter, higherChapter) = if (manga.sortDescending()) {

View File

@ -13,7 +13,7 @@ class GetApplicationRelease(
) { ) {
private val lastChecked: Preference<Long> by lazy { private val lastChecked: Preference<Long> by lazy {
preferenceStore.getLong("last_app_check", 0) preferenceStore.getLong(Preference.appStateKey("last_app_check"), 0)
} }
suspend fun await(arguments: Arguments): Result { suspend fun await(arguments: Arguments): Result {

View File

@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
@ -126,7 +125,6 @@ private fun <T> WheelPicker(
snapshotFlow { lazyListState.firstVisibleItemScrollOffset } snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.map { calculateSnappedItemIndex(lazyListState) } .map { calculateSnappedItemIndex(lazyListState) }
.distinctUntilChanged() .distinctUntilChanged()
.drop(1)
.collectLatest { .collectLatest {
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
internalOnSelectionChanged(it) internalOnSelectionChanged(it)

View File

@ -1,17 +1,33 @@
package tachiyomi.presentation.core.components.material package tachiyomi.presentation.core.components.material
import androidx.compose.animation.core.animate
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier 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 androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.pow
/** /**
* @param refreshing Whether the layout is currently refreshing * @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 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 indicatorPadding Content padding for the indicator, to inset the indicator in if required.
* @param content The content containing a vertically scrollable composable. * @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 @Composable
fun PullRefresh( fun PullRefresh(
refreshing: Boolean, refreshing: Boolean,
enabled: () -> Boolean,
onRefresh: () -> Unit, onRefresh: () -> Unit,
enabled: Boolean, modifier: Modifier = Modifier,
indicatorPadding: PaddingValues = PaddingValues(0.dp), indicatorPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val state = rememberPullRefreshState( val state = rememberPullToRefreshState(
refreshing = refreshing, extraVerticalOffset = indicatorPadding.calculateTopPadding(),
onRefresh = onRefresh, enabled = enabled,
) )
if (state.isRefreshing) {
LaunchedEffect(true) {
onRefresh()
}
}
LaunchedEffect(refreshing) {
if (refreshing && !state.isRefreshing) {
state.startRefreshAnimated()
} else if (!refreshing && state.isRefreshing) {
state.endRefreshAnimated()
}
}
Box(Modifier.pullRefresh(state, enabled)) { Box(modifier.nestedScroll(state.nestedScrollConnection)) {
content() content()
Box( val contentPadding = remember(indicatorPadding) {
Modifier object : PaddingValues {
.padding(indicatorPadding) override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
.matchParentSize() indicatorPadding.calculateLeftPadding(layoutDirection)
.clipToBounds(),
) { override fun calculateTopPadding(): Dp = 0.dp
PullRefreshIndicator(
refreshing = refreshing, override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
indicatorPadding.calculateRightPadding(layoutDirection)
override fun calculateBottomPadding(): Dp =
indicatorPadding.calculateBottomPadding()
}
}
PullToRefreshContainer(
state = state, state = state,
modifier = Modifier.align(Alignment.TopCenter), modifier = Modifier
backgroundColor = MaterialTheme.colorScheme.primary, .align(Alignment.TopCenter)
contentColor = MaterialTheme.colorScheme.onPrimary, .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<PullToRefreshStateImpl, Boolean>(
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
} }