mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
Merge branch 'sync-part-final' into feat/add-sync-triggers-experimental
This commit is contained in:
commit
5426af878e
@ -282,7 +282,10 @@ private fun MangaScreenSmallImpl(
|
|||||||
onBackClicked()
|
onBackClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackHandler(onBack = internalOnBackPressed)
|
BackHandler(
|
||||||
|
enabled = isAnySelected,
|
||||||
|
onBack = { onAllChapterSelected(false) },
|
||||||
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -540,7 +543,10 @@ fun MangaScreenLargeImpl(
|
|||||||
onBackClicked()
|
onBackClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackHandler(onBack = internalOnBackPressed)
|
BackHandler(
|
||||||
|
enabled = isAnySelected,
|
||||||
|
onBack = { onAllChapterSelected(false) },
|
||||||
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
|
@ -3,6 +3,10 @@ package eu.kanade.presentation.manga.components
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -25,15 +29,18 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
@ -48,11 +55,13 @@ import eu.kanade.presentation.components.DropdownMenu
|
|||||||
import eu.kanade.presentation.manga.EditCoverAction
|
import eu.kanade.presentation.manga.EditCoverAction
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import soup.compose.material.motion.MotionConstants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaCoverDialog(
|
fun MangaCoverDialog(
|
||||||
@ -151,10 +160,32 @@ fun MangaCoverDialog(
|
|||||||
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||||
|
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
PredictiveBackHandler { progress ->
|
||||||
|
try {
|
||||||
|
progress.collect { backEvent ->
|
||||||
|
scale = lerp(1f, 0.8f, LinearOutSlowInEasing.transform(backEvent.progress))
|
||||||
|
}
|
||||||
|
onDismissRequest()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickableNoIndication(onClick = onDismissRequest),
|
.clickableNoIndication(onClick = onDismissRequest)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = {
|
factory = {
|
||||||
|
@ -1,13 +1,54 @@
|
|||||||
package eu.kanade.presentation.util
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.activity.BackEventCompat
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.movableContentOf
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.BlurEffect
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.ColorMatrix
|
||||||
|
import androidx.compose.ui.graphics.Paint
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.ScreenModelStore
|
import cafe.adriel.voyager.core.model.ScreenModelStore
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
@ -16,14 +57,25 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
|||||||
import cafe.adriel.voyager.core.stack.StackEvent
|
import cafe.adriel.voyager.core.stack.StackEvent
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
||||||
|
import eu.kanade.tachiyomi.util.view.getWindowRadius
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
|
import soup.compose.material.motion.MotionConstants
|
||||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
import soup.compose.material.motion.animation.materialSharedAxisX
|
||||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For invoking back press to the parent activity
|
* For invoking back press to the parent activity
|
||||||
@ -57,17 +109,299 @@ interface AssistContentScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultNavigatorScreenTransition(navigator: Navigator) {
|
fun DefaultNavigatorScreenTransition(
|
||||||
val slideDistance = rememberSlideDistance()
|
navigator: Navigator,
|
||||||
ScreenTransition(
|
modifier: Modifier = Modifier,
|
||||||
navigator = navigator,
|
) {
|
||||||
transition = {
|
val scope = rememberCoroutineScope()
|
||||||
materialSharedAxisX(
|
val view = LocalView.current
|
||||||
forward = navigator.lastEvent != StackEvent.Pop,
|
val handler = remember {
|
||||||
slideDistance = slideDistance,
|
OnBackHandler(
|
||||||
|
scope = scope,
|
||||||
|
windowCornerRadius = view.getWindowRadius(),
|
||||||
|
onBackPressed = navigator::pop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PredictiveBackHandler(enabled = navigator.canPop) { progress ->
|
||||||
|
progress
|
||||||
|
.onStart { handler.reset() }
|
||||||
|
.onCompletion { e ->
|
||||||
|
if (e == null) {
|
||||||
|
handler.onBackConfirmed()
|
||||||
|
} else {
|
||||||
|
handler.onBackCancelled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collect(handler::onBackEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = modifier.onSizeChanged { handler.updateContainerSize(it.toSize()) }) {
|
||||||
|
val currentSceneEntry = navigator.lastItem
|
||||||
|
val showPrev by remember {
|
||||||
|
derivedStateOf { handler.scale < 1f || handler.translationY != 0f }
|
||||||
|
}
|
||||||
|
val visibleItems = remember(currentSceneEntry, showPrev) {
|
||||||
|
if (showPrev) {
|
||||||
|
val prevSceneEntry = navigator.items.getOrNull(navigator.size - 2)
|
||||||
|
listOfNotNull(currentSceneEntry, prevSceneEntry)
|
||||||
|
} else {
|
||||||
|
listOfNotNull(currentSceneEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val slideDistance = rememberSlideDistance()
|
||||||
|
|
||||||
|
val screenContent = remember {
|
||||||
|
movableContentOf<Screen> { screen ->
|
||||||
|
navigator.saveableState("transition", screen) {
|
||||||
|
screen.Content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleItems.forEachIndexed { index, backStackEntry ->
|
||||||
|
val isPrev = index == 1 && visibleItems.size > 1
|
||||||
|
if (!isPrev) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = backStackEntry,
|
||||||
|
transitionSpec = {
|
||||||
|
val forward = navigator.lastEvent != StackEvent.Pop
|
||||||
|
if (!forward && !handler.isReady) {
|
||||||
|
// Pop screen without animation when predictive back is in use
|
||||||
|
EnterTransition.None togetherWith ExitTransition.None
|
||||||
|
} else {
|
||||||
|
materialSharedAxisX(
|
||||||
|
forward = forward,
|
||||||
|
slideDistance = slideDistance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(1f)
|
||||||
|
.graphicsLayer {
|
||||||
|
this.alpha = handler.alpha
|
||||||
|
this.transformOrigin = TransformOrigin(
|
||||||
|
pivotFractionX = if (handler.swipeEdge == BackEventCompat.EDGE_LEFT) 0.8f else 0.2f,
|
||||||
|
pivotFractionY = 0.5f,
|
||||||
|
)
|
||||||
|
this.scaleX = handler.scale
|
||||||
|
this.scaleY = handler.scale
|
||||||
|
this.translationY = handler.translationY
|
||||||
|
this.clip = true
|
||||||
|
this.shape = if (showPrev) {
|
||||||
|
RoundedCornerShape(handler.windowCornerRadius.toFloat())
|
||||||
|
} else {
|
||||||
|
RectangleShape
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
if (showPrev) {
|
||||||
|
Modifier.pointerInput(Unit) {
|
||||||
|
// Animated content should not be interactive
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content = {
|
||||||
|
if (visibleItems.size == 2 && visibleItems.getOrNull(1) == it) {
|
||||||
|
// Avoid drawing previous screen
|
||||||
|
return@AnimatedContent
|
||||||
|
}
|
||||||
|
screenContent(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(0f)
|
||||||
|
.drawWithCache {
|
||||||
|
val bounds = Rect(Offset.Zero, size)
|
||||||
|
val matrix = ColorMatrix().apply {
|
||||||
|
// Reduce saturation and brightness
|
||||||
|
setToSaturation(lerp(1f, 0.95f, handler.alpha))
|
||||||
|
set(0, 4, lerp(0f, -25f, handler.alpha))
|
||||||
|
set(1, 4, lerp(0f, -25f, handler.alpha))
|
||||||
|
set(2, 4, lerp(0f, -25f, handler.alpha))
|
||||||
|
}
|
||||||
|
val paint = Paint().apply { colorFilter = ColorFilter.colorMatrix(matrix) }
|
||||||
|
onDrawWithContent {
|
||||||
|
drawIntoCanvas {
|
||||||
|
it.saveLayer(bounds, paint)
|
||||||
|
drawContent()
|
||||||
|
it.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.graphicsLayer {
|
||||||
|
val blurRadius = 5.dp.toPx() * handler.alpha
|
||||||
|
renderEffect = if (blurRadius > 0f) {
|
||||||
|
BlurEffect(blurRadius, blurRadius)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
// bg content should not be interactive
|
||||||
|
},
|
||||||
|
content = { screenContent(backStackEntry) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(currentSceneEntry) {
|
||||||
|
// Reset *after* the screen is popped successfully
|
||||||
|
// so that the correct transition is applied
|
||||||
|
handler.setReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
private class OnBackHandler(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
val windowCornerRadius: Int,
|
||||||
|
private val onBackPressed: () -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var isReady = true
|
||||||
|
private set
|
||||||
|
|
||||||
|
var alpha by mutableFloatStateOf(1f)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var scale by mutableFloatStateOf(1f)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var translationY by mutableFloatStateOf(0f)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var swipeEdge by mutableIntStateOf(BackEventCompat.EDGE_LEFT)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var containerSize = Size.Zero
|
||||||
|
private var startPointY = Float.NaN
|
||||||
|
|
||||||
|
var isPredictiveBack by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var animationJob: Job? = null
|
||||||
|
set(value) {
|
||||||
|
isReady = false
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateContainerSize(size: Size) {
|
||||||
|
containerSize = size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setReady() {
|
||||||
|
reset()
|
||||||
|
animationJob?.cancel()
|
||||||
|
animationJob = null
|
||||||
|
isReady = true
|
||||||
|
isPredictiveBack = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
startPointY = Float.NaN
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackEvent(backEvent: BackEventCompat) {
|
||||||
|
if (!isReady) return
|
||||||
|
isPredictiveBack = true
|
||||||
|
swipeEdge = backEvent.swipeEdge
|
||||||
|
|
||||||
|
val progress = LinearOutSlowInEasing.transform(backEvent.progress)
|
||||||
|
scale = lerp(1f, 0.85f, progress)
|
||||||
|
|
||||||
|
if (startPointY.isNaN()) {
|
||||||
|
startPointY = backEvent.touchY
|
||||||
|
}
|
||||||
|
val deltaYRatio = (backEvent.touchY - startPointY) / containerSize.height
|
||||||
|
val translateYDistance = containerSize.height / 20
|
||||||
|
translationY = sin(deltaYRatio * PI * 0.5).toFloat() * translateYDistance * progress
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackConfirmed() {
|
||||||
|
if (!isReady) return
|
||||||
|
if (isPredictiveBack) {
|
||||||
|
// Continue predictive animation and pop the screen
|
||||||
|
val animationSpec = tween<Float>(
|
||||||
|
durationMillis = MotionConstants.DefaultMotionDuration,
|
||||||
|
easing = FastOutSlowInEasing,
|
||||||
)
|
)
|
||||||
},
|
animationJob = scope.launch {
|
||||||
)
|
try {
|
||||||
|
listOf(
|
||||||
|
async {
|
||||||
|
animate(
|
||||||
|
initialValue = alpha,
|
||||||
|
targetValue = 0f,
|
||||||
|
animationSpec = animationSpec,
|
||||||
|
) { value, _ ->
|
||||||
|
alpha = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = scale - 0.05f,
|
||||||
|
animationSpec = animationSpec,
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).awaitAll()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// no-op
|
||||||
|
} finally {
|
||||||
|
onBackPressed()
|
||||||
|
alpha = 1f
|
||||||
|
translationY = 0f
|
||||||
|
scale = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pop right away and use default transition
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackCancelled() {
|
||||||
|
// Reset states
|
||||||
|
isPredictiveBack = false
|
||||||
|
animationJob = scope.launch {
|
||||||
|
listOf(
|
||||||
|
async {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
animate(
|
||||||
|
initialValue = alpha,
|
||||||
|
targetValue = 1f,
|
||||||
|
) { value, _ ->
|
||||||
|
alpha = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
animate(
|
||||||
|
initialValue = translationY,
|
||||||
|
targetValue = 0f,
|
||||||
|
) { value, _ ->
|
||||||
|
translationY = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).awaitAll()
|
||||||
|
|
||||||
|
isReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -168,47 +168,28 @@ class SyncManager(
|
|||||||
return handler.awaitList { mangasQueries.getAllManga(::mapManga) }
|
return handler.awaitList { mangasQueries.getAllManga(::mapManga) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private suspend fun isMangaDifferent(localManga: Manga, remoteManga: BackupManga): Boolean {
|
||||||
* Determines if there are differences between the local manga details and categories and their corresponding
|
|
||||||
* remote backup versions. This is used to identify changes or updates that might have occurred.
|
|
||||||
*
|
|
||||||
* @param localManga The manga object from the local database of type [Manga].
|
|
||||||
* @param backupManga The manga object from the remote backup of type [BackupManga].
|
|
||||||
*
|
|
||||||
* @return Boolean indicating whether there are differences. Returns true if any of the following is true:
|
|
||||||
* - The local manga details differ from the remote backup manga details.
|
|
||||||
* - The chapters of the local manga differ from the chapters of the remote backup manga.
|
|
||||||
* - The categories of the local manga differ from the categories of the remote backup manga.
|
|
||||||
*
|
|
||||||
* This function uses a combination of direct property comparisons and delegated comparison functions
|
|
||||||
* to assess differences across manga details, chapters, and categories. It relies on proper conversion
|
|
||||||
* of remote manga and chapters to the local format for accurate comparison.
|
|
||||||
*/
|
|
||||||
private suspend fun isMangaDifferent(localManga: Manga, backupManga: BackupManga): Boolean {
|
|
||||||
val remoteManga = backupManga.getMangaImpl()
|
|
||||||
val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id, 0).executeAsList() }
|
val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id, 0).executeAsList() }
|
||||||
val localCategories = getCategories.await(localManga.id).map { it.order }
|
val localCategories = getCategories.await(localManga.id).map { it.order }
|
||||||
|
|
||||||
return localManga != remoteManga || areChaptersDifferent(localChapters, backupManga.chapters) || localCategories != backupManga.categories
|
return localManga.source != remoteManga.source ||
|
||||||
|
localManga.url != remoteManga.url ||
|
||||||
|
localManga.title != remoteManga.title ||
|
||||||
|
localManga.artist != remoteManga.artist ||
|
||||||
|
localManga.author != remoteManga.author ||
|
||||||
|
localManga.description != remoteManga.description ||
|
||||||
|
localManga.genre != remoteManga.genre ||
|
||||||
|
localManga.status.toInt() != remoteManga.status ||
|
||||||
|
localManga.thumbnailUrl != remoteManga.thumbnailUrl ||
|
||||||
|
localManga.dateAdded != remoteManga.dateAdded ||
|
||||||
|
localManga.chapterFlags.toInt() != remoteManga.chapterFlags ||
|
||||||
|
localManga.favorite != remoteManga.favorite ||
|
||||||
|
localManga.viewerFlags.toInt() != remoteManga.viewer_flags ||
|
||||||
|
localManga.updateStrategy != remoteManga.updateStrategy ||
|
||||||
|
areChaptersDifferent(localChapters, remoteManga.chapters) ||
|
||||||
|
localCategories != remoteManga.categories
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there are any differences between a list of local chapters and a list of backup chapters.
|
|
||||||
* This function is used to determine if updates or changes have occurred between the two sets of chapters.
|
|
||||||
*
|
|
||||||
* @param localChapters The list of chapters from the local source, of type [Chapters].
|
|
||||||
* @param remoteChapters The list of chapters from the remote backup source, of type [BackupChapter].
|
|
||||||
*
|
|
||||||
* @return Boolean indicating whether there are differences. Returns true if any of the following is true:
|
|
||||||
* - The count of local and remote chapters differs.
|
|
||||||
* - Any corresponding chapters (matched by URL) have differing attributes including name, scanlator,
|
|
||||||
* read status, bookmark status, last page read, chapter number, source order, fetch date, upload date,
|
|
||||||
* or last modified date.
|
|
||||||
*
|
|
||||||
* Each chapter is compared based on a set of fields that define its content and state. If any of these fields
|
|
||||||
* differ between the local chapter and its corresponding remote chapter, it is considered a difference.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private fun areChaptersDifferent(localChapters: List<Chapters>, remoteChapters: List<BackupChapter>): Boolean {
|
private fun areChaptersDifferent(localChapters: List<Chapters>, remoteChapters: List<BackupChapter>): Boolean {
|
||||||
// Early return if the sizes are different
|
// Early return if the sizes are different
|
||||||
if (localChapters.size != remoteChapters.size) {
|
if (localChapters.size != remoteChapters.size) {
|
||||||
@ -234,8 +215,7 @@ class SyncManager(
|
|||||||
localChapter.chapter_number != remoteChapter.chapterNumber ||
|
localChapter.chapter_number != remoteChapter.chapterNumber ||
|
||||||
localChapter.source_order != remoteChapter.sourceOrder ||
|
localChapter.source_order != remoteChapter.sourceOrder ||
|
||||||
localChapter.date_fetch != remoteChapter.dateFetch ||
|
localChapter.date_fetch != remoteChapter.dateFetch ||
|
||||||
localChapter.date_upload != remoteChapter.dateUpload ||
|
localChapter.date_upload != remoteChapter.dateUpload
|
||||||
localChapter.last_modified_at != remoteChapter.lastModifiedAt
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,94 +80,93 @@ abstract class SyncService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges two lists of SyncManga objects, prioritizing the manga with the most recent lastModifiedAt value.
|
* Merges two lists of BackupManga objects, selecting the most recent manga based on the lastModifiedAt value.
|
||||||
* If lastModifiedAt is null, the function defaults to Instant.MIN for comparison purposes.
|
* If lastModifiedAt is null for a manga, it treats that manga as the oldest possible for comparison purposes.
|
||||||
|
* This function is designed to reconcile local and remote manga lists, ensuring the most up-to-date manga is retained.
|
||||||
*
|
*
|
||||||
* @param localMangaList The list of local SyncManga objects.
|
* @param localMangaList The list of local BackupManga objects or null.
|
||||||
* @param remoteMangaList The list of remote SyncManga objects.
|
* @param remoteMangaList The list of remote BackupManga objects or null.
|
||||||
* @return The merged list of SyncManga objects.
|
* @return A list of BackupManga objects, each representing the most recent version of the manga from either local or remote sources.
|
||||||
*/
|
*/
|
||||||
private fun mergeMangaLists(
|
private fun mergeMangaLists(
|
||||||
localMangaList: List<BackupManga>?,
|
localMangaList: List<BackupManga>?,
|
||||||
remoteMangaList: List<BackupManga>?,
|
remoteMangaList: List<BackupManga>?,
|
||||||
): List<BackupManga> {
|
): List<BackupManga> {
|
||||||
if (localMangaList == null) return remoteMangaList ?: emptyList()
|
// Convert null lists to empty to simplify logic
|
||||||
if (remoteMangaList == null) return localMangaList
|
val localMangaListSafe = localMangaList.orEmpty()
|
||||||
|
val remoteMangaListSafe = remoteMangaList.orEmpty()
|
||||||
|
|
||||||
val localMangaMap = localMangaList.associateBy { Pair(it.source, it.url) }
|
// Associate both local and remote manga by their unique keys (source and url)
|
||||||
val remoteMangaMap = remoteMangaList.associateBy { Pair(it.source, it.url) }
|
val localMangaMap = localMangaListSafe.associateBy { Pair(it.source, it.url) }
|
||||||
|
val remoteMangaMap = remoteMangaListSafe.associateBy { Pair(it.source, it.url) }
|
||||||
|
|
||||||
val mergedMangaMap = mutableMapOf<Pair<Long, String>, BackupManga>()
|
// Prepare to merge both sets of manga
|
||||||
|
return (localMangaMap.keys + remoteMangaMap.keys).mapNotNull { key ->
|
||||||
|
val local = localMangaMap[key]
|
||||||
|
val remote = remoteMangaMap[key]
|
||||||
|
|
||||||
localMangaMap.forEach { (key, localManga) ->
|
when {
|
||||||
val remoteManga = remoteMangaMap[key]
|
local != null && remote == null -> local
|
||||||
if (remoteManga != null) {
|
local == null && remote != null -> remote
|
||||||
val localMangaLastModifiedAtInstant = Instant.ofEpochMilli(localManga.lastModifiedAt)
|
local != null && remote != null -> {
|
||||||
val remoteMangaLastModifiedAtInstant = Instant.ofEpochMilli(remoteManga.lastModifiedAt)
|
// Compare last modified times and merge chapters
|
||||||
val mergedChapters = mergeChapters(localManga.chapters, remoteManga.chapters)
|
val localTime = Instant.ofEpochMilli(local.lastModifiedAt)
|
||||||
// Keep the more recent manga
|
val remoteTime = Instant.ofEpochMilli(remote.lastModifiedAt)
|
||||||
if (localMangaLastModifiedAtInstant.isAfter(remoteMangaLastModifiedAtInstant)) {
|
val mergedChapters = mergeChapters(local.chapters, remote.chapters)
|
||||||
mergedMangaMap[key] = localManga.copy(chapters = mergedChapters)
|
|
||||||
} else {
|
if (localTime >= remoteTime) {
|
||||||
mergedMangaMap[key] = remoteManga.copy(chapters = mergedChapters)
|
local.copy(chapters = mergedChapters)
|
||||||
|
} else {
|
||||||
|
remote.copy(chapters = mergedChapters)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
else -> null // This case occurs if both are null, which shouldn't happen but is handled for completeness.
|
||||||
// If there is no corresponding remote manga, keep the local one
|
|
||||||
mergedMangaMap[key] = localManga
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any remote manga that doesn't exist locally
|
|
||||||
remoteMangaMap.forEach { (key, remoteManga) ->
|
|
||||||
if (!mergedMangaMap.containsKey(key)) {
|
|
||||||
mergedMangaMap[key] = remoteManga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedMangaMap.values.toList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges two lists of SyncChapter objects, prioritizing the chapter with the most recent lastModifiedAt value.
|
* Merges two lists of BackupChapter objects, selecting the most recent chapter based on the lastModifiedAt value.
|
||||||
* If lastModifiedAt is null, the function defaults to Instant.MIN for comparison purposes.
|
* If lastModifiedAt is null for a chapter, it treats that chapter as the oldest possible for comparison purposes.
|
||||||
|
* This function is designed to reconcile local and remote chapter lists, ensuring the most up-to-date chapter is retained.
|
||||||
*
|
*
|
||||||
* @param localChapters The list of local SyncChapter objects.
|
* @param localChapters The list of local BackupChapter objects.
|
||||||
* @param remoteChapters The list of remote SyncChapter objects.
|
* @param remoteChapters The list of remote BackupChapter objects.
|
||||||
* @return The merged list of SyncChapter objects.
|
* @return A list of BackupChapter objects, each representing the most recent version of the chapter from either local or remote sources.
|
||||||
|
*
|
||||||
|
* - This function is used in scenarios where local and remote chapter lists need to be synchronized.
|
||||||
|
* - It iterates over the union of the URLs from both local and remote chapters.
|
||||||
|
* - For each URL, it compares the corresponding local and remote chapters based on the lastModifiedAt value.
|
||||||
|
* - If only one source (local or remote) has the chapter for a URL, that chapter is used.
|
||||||
|
* - If both sources have the chapter, the one with the more recent lastModifiedAt value is chosen.
|
||||||
|
* - If lastModifiedAt is null or missing, the chapter is considered the oldest for safety, ensuring that any chapter with a valid timestamp is preferred.
|
||||||
|
* - The resulting list contains the most recent chapters from the combined set of local and remote chapters.
|
||||||
*/
|
*/
|
||||||
private fun mergeChapters(
|
private fun mergeChapters(
|
||||||
localChapters: List<BackupChapter>,
|
localChapters: List<BackupChapter>,
|
||||||
remoteChapters: List<BackupChapter>,
|
remoteChapters: List<BackupChapter>,
|
||||||
): List<BackupChapter> {
|
): List<BackupChapter> {
|
||||||
|
// Associate chapters by URL for both local and remote
|
||||||
val localChapterMap = localChapters.associateBy { it.url }
|
val localChapterMap = localChapters.associateBy { it.url }
|
||||||
val remoteChapterMap = remoteChapters.associateBy { it.url }
|
val remoteChapterMap = remoteChapters.associateBy { it.url }
|
||||||
val mergedChapterMap = mutableMapOf<String, BackupChapter>()
|
|
||||||
|
|
||||||
localChapterMap.forEach { (url, localChapter) ->
|
// Merge both chapter maps
|
||||||
|
return (localChapterMap.keys + remoteChapterMap.keys).mapNotNull { url ->
|
||||||
|
// Determine the most recent chapter by comparing lastModifiedAt, considering null as Instant.MIN
|
||||||
|
val localChapter = localChapterMap[url]
|
||||||
val remoteChapter = remoteChapterMap[url]
|
val remoteChapter = remoteChapterMap[url]
|
||||||
if (remoteChapter != null) {
|
|
||||||
val localInstant = localChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) }
|
|
||||||
val remoteInstant = remoteChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) }
|
|
||||||
|
|
||||||
val mergedChapter =
|
when {
|
||||||
if (localInstant > remoteInstant) {
|
localChapter != null && remoteChapter == null -> localChapter
|
||||||
localChapter
|
localChapter == null && remoteChapter != null -> remoteChapter
|
||||||
} else {
|
localChapter != null && remoteChapter != null -> {
|
||||||
remoteChapter
|
val localInstant = localChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) } ?: Instant.MIN
|
||||||
}
|
val remoteInstant = remoteChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) } ?: Instant.MIN
|
||||||
mergedChapterMap[url] = mergedChapter
|
if (localInstant >= remoteInstant) localChapter else remoteChapter
|
||||||
} else {
|
}
|
||||||
mergedChapterMap[url] = localChapter
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteChapterMap.forEach { (url, remoteChapter) ->
|
|
||||||
if (!mergedChapterMap.containsKey(url)) {
|
|
||||||
mergedChapterMap[url] = remoteChapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedChapterMap.values.toList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.ui.home
|
package eu.kanade.tachiyomi.ui.home
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
@ -23,13 +26,20 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
@ -49,6 +59,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import soup.compose.material.motion.MotionConstants
|
||||||
import soup.compose.material.motion.animation.materialFadeThroughIn
|
import soup.compose.material.motion.animation.materialFadeThroughIn
|
||||||
import soup.compose.material.motion.animation.materialFadeThroughOut
|
import soup.compose.material.motion.animation.materialFadeThroughOut
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
@ -59,6 +70,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
object HomeScreen : Screen() {
|
object HomeScreen : Screen() {
|
||||||
|
|
||||||
@ -80,6 +92,8 @@ object HomeScreen : Screen() {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
|
||||||
TabNavigator(
|
TabNavigator(
|
||||||
tab = LibraryTab,
|
tab = LibraryTab,
|
||||||
key = TabNavigatorKey,
|
key = TabNavigatorKey,
|
||||||
@ -118,6 +132,11 @@ object HomeScreen : Screen() {
|
|||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
transformOrigin = TransformOrigin(0.5f, 1f)
|
||||||
|
}
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.consumeWindowInsets(contentPadding),
|
.consumeWindowInsets(contentPadding),
|
||||||
) {
|
) {
|
||||||
@ -138,10 +157,30 @@ object HomeScreen : Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val goToLibraryTab = { tabNavigator.current = LibraryTab }
|
val goToLibraryTab = { tabNavigator.current = LibraryTab }
|
||||||
BackHandler(
|
|
||||||
enabled = tabNavigator.current != LibraryTab,
|
var handlingBack by remember { mutableStateOf(false) }
|
||||||
onBack = goToLibraryTab,
|
PredictiveBackHandler(enabled = handlingBack || tabNavigator.current != LibraryTab) { progress ->
|
||||||
)
|
handlingBack = true
|
||||||
|
val currentTab = tabNavigator.current
|
||||||
|
try {
|
||||||
|
progress.collect { backEvent ->
|
||||||
|
scale = lerp(1f, 0.92f, LinearOutSlowInEasing.transform(backEvent.progress))
|
||||||
|
tabNavigator.current = if (backEvent.progress > 0.25f) tabs[0] else currentTab
|
||||||
|
}
|
||||||
|
goToLibraryTab()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
tabNavigator.current = currentTab
|
||||||
|
} finally {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
handlingBack = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
launch {
|
launch {
|
||||||
|
@ -11,7 +11,6 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
@ -223,14 +222,13 @@ class MainActivity : BaseActivity() {
|
|||||||
contentWindowInsets = scaffoldInsets,
|
contentWindowInsets = scaffoldInsets,
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
// Consume insets already used by app state banners
|
// Consume insets already used by app state banners
|
||||||
Box(
|
// Shows current screen
|
||||||
|
DefaultNavigatorScreenTransition(
|
||||||
|
navigator = navigator,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.consumeWindowInsets(contentPadding),
|
.consumeWindowInsets(contentPadding),
|
||||||
) {
|
)
|
||||||
// Shows current screen
|
|
||||||
DefaultNavigatorScreenTransition(navigator = navigator)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pop source-related screens when incognito mode is turned off
|
// Pop source-related screens when incognito mode is turned off
|
||||||
|
@ -40,6 +40,7 @@ class SettingsScreen(
|
|||||||
Destination.Tracking.id -> SettingsTrackingScreen
|
Destination.Tracking.id -> SettingsTrackingScreen
|
||||||
else -> SettingsMainScreen
|
else -> SettingsMainScreen
|
||||||
},
|
},
|
||||||
|
onBackPressed = null,
|
||||||
content = {
|
content = {
|
||||||
val pop: () -> Unit = {
|
val pop: () -> Unit = {
|
||||||
if (it.canPop) {
|
if (it.canPop) {
|
||||||
@ -61,6 +62,7 @@ class SettingsScreen(
|
|||||||
Destination.Tracking.id -> SettingsTrackingScreen
|
Destination.Tracking.id -> SettingsTrackingScreen
|
||||||
else -> SettingsAppearanceScreen
|
else -> SettingsAppearanceScreen
|
||||||
},
|
},
|
||||||
|
onBackPressed = null,
|
||||||
) {
|
) {
|
||||||
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
|
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
|
||||||
TwoPanelBox(
|
TwoPanelBox(
|
||||||
|
@ -4,9 +4,11 @@ package eu.kanade.tachiyomi.util.view
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.RoundedCorner
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@ -95,3 +97,22 @@ fun View?.isVisibleOnScreen(): Boolean {
|
|||||||
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
|
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
|
||||||
return actualPosition.intersect(screen)
|
return actualPosition.intersect(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns window radius (in pixel) applied to this view
|
||||||
|
*/
|
||||||
|
fun View.getWindowRadius(): Int {
|
||||||
|
val rad = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val windowInsets = rootWindowInsets
|
||||||
|
listOfNotNull(
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT),
|
||||||
|
)
|
||||||
|
.minOfOrNull { it.radius }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return rad ?: 0
|
||||||
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package tachiyomi.presentation.core.components
|
package tachiyomi.presentation.core.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
@ -26,6 +30,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -34,8 +39,11 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
@ -45,14 +53,14 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdaptiveSheet(
|
fun AdaptiveSheet(
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
@ -91,6 +99,11 @@ fun AdaptiveSheet(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.predictiveBackAnimation(
|
||||||
|
enabled = remember { derivedStateOf { alpha > 0f } }.value,
|
||||||
|
transformOrigin = TransformOrigin.Center,
|
||||||
|
onBack = internalOnDismissRequest,
|
||||||
|
)
|
||||||
.requiredWidthIn(max = 460.dp)
|
.requiredWidthIn(max = 460.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
@ -103,7 +116,6 @@ fun AdaptiveSheet(
|
|||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
content = {
|
content = {
|
||||||
BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
|
|
||||||
content()
|
content()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -145,6 +157,11 @@ fun AdaptiveSheet(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.predictiveBackAnimation(
|
||||||
|
enabled = anchoredDraggableState.targetValue == 0,
|
||||||
|
transformOrigin = TransformOrigin(0.5f, 1f),
|
||||||
|
onBack = internalOnDismissRequest,
|
||||||
|
)
|
||||||
.widthIn(max = 460.dp)
|
.widthIn(max = 460.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
@ -184,10 +201,6 @@ fun AdaptiveSheet(
|
|||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
content = {
|
content = {
|
||||||
BackHandler(
|
|
||||||
enabled = anchoredDraggableState.targetValue == 0,
|
|
||||||
onBack = internalOnDismissRequest,
|
|
||||||
)
|
|
||||||
content()
|
content()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -257,3 +270,37 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection()
|
|||||||
@JvmName("offsetToFloat")
|
@JvmName("offsetToFloat")
|
||||||
private fun Offset.toFloat(): Float = this.y
|
private fun Offset.toFloat(): Float = this.y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Modifier.predictiveBackAnimation(
|
||||||
|
enabled: Boolean,
|
||||||
|
transformOrigin: TransformOrigin,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) = composed {
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
PredictiveBackHandler(enabled = enabled) { progress ->
|
||||||
|
try {
|
||||||
|
progress.collect { backEvent ->
|
||||||
|
scale = lerp(1f, 0.85f, LinearOutSlowInEasing.transform(backEvent.progress))
|
||||||
|
}
|
||||||
|
// Completion
|
||||||
|
onBack()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// Cancellation
|
||||||
|
} finally {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Modifier.graphicsLayer {
|
||||||
|
this.scaleX = scale
|
||||||
|
this.scaleY = scale
|
||||||
|
this.transformOrigin = transformOrigin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
||||||
|
Loading…
Reference in New Issue
Block a user