[Reader] ReaderOptions TopSheet

This commit is contained in:
tom5079
2021-12-23 22:37:38 +09:00
parent 4625bb5806
commit 7e52a2e296
5 changed files with 405 additions and 72 deletions

View File

@@ -45,7 +45,7 @@ import xyz.quaver.pupil.sources.composable.ModalTopSheetState.Expanded
import xyz.quaver.pupil.sources.composable.ModalTopSheetState.Hidden import xyz.quaver.pupil.sources.composable.ModalTopSheetState.Hidden
import kotlin.math.roundToInt import kotlin.math.roundToInt
class ModalTopSheetLayout( class ModalTopSheetLayoutShape(
private val cornerRadius: Dp, private val cornerRadius: Dp,
private val handleRadius: Dp private val handleRadius: Dp
): Shape { ): Shape {
@@ -152,7 +152,7 @@ private fun Scrim(
@Composable @Composable
@ExperimentalMaterialApi @ExperimentalMaterialApi
fun SearchOptionDrawer( fun ModalTopSheetLayout(
drawerContent: @Composable ColumnScope.() -> Unit, drawerContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
drawerCornerRadius: Dp = SearchOptionDrawerDefaults.CornerRadius, drawerCornerRadius: Dp = SearchOptionDrawerDefaults.CornerRadius,
@@ -229,7 +229,7 @@ fun SearchOptionDrawer(
.onGloballyPositioned { .onGloballyPositioned {
sheetHeight = it.size.height.toFloat() sheetHeight = it.size.height.toFloat()
}, },
shape = ModalTopSheetLayout(drawerCornerRadius, drawerHandleRadius), shape = ModalTopSheetLayoutShape(drawerCornerRadius, drawerHandleRadius),
elevation = drawerElevation, elevation = drawerElevation,
color = drawerBackgroundColor, color = drawerBackgroundColor,
contentColor = drawerContentColor contentColor = drawerContentColor

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.sources.composable
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@@ -28,11 +29,21 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material.icons.filled.BrokenImage import androidx.compose.material.icons.filled.BrokenImage
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -50,6 +61,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -63,12 +75,93 @@ import xyz.quaver.graphics.subsampledimage.*
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.db.AppDatabase import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.proto.ReaderOptions
import xyz.quaver.pupil.proto.settingsDataStore
import xyz.quaver.pupil.ui.theme.Orange500 import xyz.quaver.pupil.ui.theme.Orange500
import xyz.quaver.pupil.util.NetworkCache import xyz.quaver.pupil.util.NetworkCache
import xyz.quaver.pupil.util.activity import xyz.quaver.pupil.util.activity
import xyz.quaver.pupil.util.rememberFileXImageSource import xyz.quaver.pupil.util.rememberFileXImageSource
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.sign
private var _singleImage: ImageVector? = null
val SingleImage: ImageVector
get() {
if (_singleImage != null) {
return _singleImage!!
}
_singleImage = materialIcon(name = "ReaderBase.SingleImage") {
materialPath {
moveTo(17.0f, 3.0f)
lineTo(7.0f, 3.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
verticalLineToRelative(14.0f)
curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
horizontalLineToRelative(10.0f)
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
lineTo(19.0f, 5.0f)
curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
close()
moveTo(17.0f, 19.0f)
lineTo(7.0f, 19.0f)
lineTo(7.0f, 5.0f)
horizontalLineToRelative(10.0f)
verticalLineToRelative(14.0f)
close()
}
}
return _singleImage!!
}
private var _doubleImage: ImageVector? = null
val DoubleImage: ImageVector
get() {
if (_doubleImage != null) {
return _doubleImage!!
}
_doubleImage = materialIcon(name = "ReaderBase.DoubleImage") {
materialPath {
moveTo(9.0f, 3.0f)
lineTo(2.0f, 3.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
verticalLineToRelative(14.0f)
curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
horizontalLineToRelative(7.0f)
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
lineTo(11.0f, 5.0f)
curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
close()
moveTo(9.0f, 19.0f)
lineTo(2.0f, 19.0f)
lineTo(2.0f, 5.0f)
horizontalLineToRelative(7.0f)
verticalLineToRelative(14.0f)
close()
moveTo(21.0f, 3.0f)
lineTo(14.0f, 3.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
verticalLineToRelative(14.0f)
curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
horizontalLineToRelative(7.0f)
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
lineTo(23.0f, 5.0f)
curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
close()
moveTo(21.0f, 19.0f)
lineTo(14.0f, 19.0f)
lineTo(14.0f, 5.0f)
horizontalLineToRelative(7.0f)
verticalLineToRelative(14.0f)
close()
}
}
return _doubleImage!!
}
open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAware { open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAware {
override val di by closestDI(app) override val di by closestDI(app)
@@ -155,7 +248,8 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
} }
} }
@OptIn(ExperimentalFoundationApi::class) @ExperimentalMaterialApi
@ExperimentalFoundationApi
@Composable @Composable
fun ReaderBase( fun ReaderBase(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -164,9 +258,26 @@ fun ReaderBase(
val context = LocalContext.current val context = LocalContext.current
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState() val scaffoldState = rememberScaffoldState()
val snackbarCoroutineScope = rememberCoroutineScope() val snackbarCoroutineScope = rememberCoroutineScope()
var scrollDirection by remember { mutableStateOf(0f) }
val handleOffset by animateDpAsState(if (model.fullscreen || scrollDirection < 0f) (-36).dp else 0.dp)
val mainReaderOptions by remember {
context.settingsDataStore.data.map { it.mainReaderOption }
}.collectAsState(ReaderOptions.getDefaultInstance())
val nestedScrollConnection = remember { object: NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
scrollDirection = available.y.sign
return Offset.Zero
}
} }
LaunchedEffect(model.fullscreen) { LaunchedEffect(model.fullscreen) {
context.activity?.window?.let { window -> context.activity?.window?.let { window ->
ViewCompat.getWindowInsetsController(window.decorView)?.let { ViewCompat.getWindowInsetsController(window.decorView)?.let {
@@ -191,10 +302,196 @@ fun ReaderBase(
} }
Box(modifier) { Box(modifier) {
ModalTopSheetLayout(
modifier = Modifier.offset(0.dp, handleOffset),
drawerContent = {
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.h6) {
Column(Modifier.padding(16.dp, 0.dp)) {
val layout = mainReaderOptions.layout
val snap = mainReaderOptions.snap
val orientation = mainReaderOptions.orientation
val padding = mainReaderOptions.padding
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Layout")
Row {
listOf(
ReaderOptions.Layout.SINGLE_PAGE to SingleImage,
ReaderOptions.Layout.DOUBLE_PAGE to DoubleImage,
ReaderOptions.Layout.AUTO to Icons.Default.AutoFixHigh
).forEach { (option, icon) ->
IconButton(onClick = {
coroutineScope.launch {
context.settingsDataStore.updateData {
it.toBuilder().setMainReaderOption(
it.mainReaderOption.toBuilder()
.setLayout(option)
.build()
).build()
}
}
}) {
Icon(
icon,
contentDescription = null,
tint =
if (layout == option) MaterialTheme.colors.secondary
else LocalContentColor.current
)
}
}
}
}
val infiniteTransition = rememberInfiniteTransition()
val isVertical =
orientation == ReaderOptions.Orientation.VERTICAL_DOWN ||
orientation == ReaderOptions.Orientation.VERTICAL_UP
val isReverse =
orientation == ReaderOptions.Orientation.VERTICAL_UP ||
orientation == ReaderOptions.Orientation.HORIZONTAL_LEFT
val animationOrientation = if (isReverse) -1f else 1f
val animationSpacing by animateFloatAsState(if (padding) 48f else 32f)
val animationOffset by infiniteTransition.animateFloat(
initialValue = animationOrientation * (if (snap) 0f else animationSpacing/2),
targetValue = animationOrientation * (if (snap) -animationSpacing else -animationSpacing/2),
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = if(snap) FastOutSlowInEasing else LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
val animationRotation by animateFloatAsState(if (isVertical) 90f else 0f)
val setOrientation: (Boolean, Boolean) -> Unit = { isVertical, isReverse ->
val orientation = when {
isVertical && !isReverse -> ReaderOptions.Orientation.VERTICAL_DOWN
isVertical && isReverse -> ReaderOptions.Orientation.VERTICAL_UP
!isVertical && !isReverse -> ReaderOptions.Orientation.HORIZONTAL_RIGHT
!isVertical && isReverse -> ReaderOptions.Orientation.HORIZONTAL_LEFT
else -> error("Invalid value")
}
coroutineScope.launch {
context.settingsDataStore.updateData {
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setOrientation(orientation)
.build()
).build()
}
}
}
Box(
modifier = Modifier
.size(48.dp)
.clipToBounds()
.rotate(animationRotation)
.align(Alignment.CenterHorizontally)
) {
for (i in 0..4)
Icon(
SingleImage,
contentDescription = null,
modifier = Modifier
.size(48.dp)
.align(Alignment.CenterStart)
.offset((animationOffset + animationSpacing * (i - 2)).dp, 0.dp)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Orientation")
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.caption) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("H")
Switch(checked = isVertical, onCheckedChange = {
setOrientation(!isVertical, isReverse)
})
Text("V")
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Reverse")
Switch(checked = isReverse, onCheckedChange = {
setOrientation(isVertical, !isReverse)
})
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Snap")
Switch(checked = snap, onCheckedChange = {
coroutineScope.launch {
context.settingsDataStore.updateData {
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setSnap(!snap)
.build()
).build()
}
}
})
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Padding")
Switch(checked = padding, onCheckedChange = {
coroutineScope.launch {
context.settingsDataStore.updateData {
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setPadding(!padding)
.build()
).build()
}
}
})
}
Box(
Modifier
.fillMaxWidth()
.height(8.dp))
}
}
}
) {
LazyColumn( LazyColumn(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.align(Alignment.TopStart), .align(Alignment.TopStart)
.nestedScroll(nestedScrollConnection),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars) contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
) { ) {
@@ -256,7 +553,8 @@ fun ReaderBase(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.TopCenter), .align(Alignment.TopCenter),
progress = model.progressList.map { if (it.isInfinite()) 1f else abs(it) }.sum() / model.progressList.size, progress = model.progressList.map { if (it.isInfinite()) 1f else abs(it) }
.sum() / model.progressList.size,
color = MaterialTheme.colors.secondary color = MaterialTheme.colors.secondary
) )
@@ -266,3 +564,4 @@ fun ReaderBase(
) )
} }
} }
}

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.sources.hitomi
import android.app.Application import android.app.Application
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -66,6 +67,7 @@ import xyz.quaver.pupil.sources.hitomi.lib.getReferer
import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage
import xyz.quaver.pupil.ui.theme.Orange500 import xyz.quaver.pupil.ui.theme.Orange500
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
class Hitomi(app: Application) : Source(), DIAware { class Hitomi(app: Application) : Source(), DIAware {
override val di by closestDI(app) override val di by closestDI(app)

View File

@@ -18,7 +18,6 @@
package xyz.quaver.pupil.sources.manatoki package xyz.quaver.pupil.sources.manatoki
import android.app.Application import android.app.Application
import android.util.LruCache
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
@@ -47,6 +46,9 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
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.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@@ -72,8 +74,6 @@ import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.* import io.ktor.client.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
@@ -89,6 +89,7 @@ import xyz.quaver.pupil.sources.manatoki.composable.*
import xyz.quaver.pupil.sources.manatoki.viewmodel.* import xyz.quaver.pupil.sources.manatoki.viewmodel.*
import xyz.quaver.pupil.ui.theme.Orange500 import xyz.quaver.pupil.ui.theme.Orange500
import kotlin.math.max import kotlin.math.max
import kotlin.math.sign
private val imageUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36" private val imageUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36"
@@ -413,6 +414,15 @@ class Manatoki(app: Application) : Source(), DIAware {
val listState = rememberLazyListState() val listState = rememberLazyListState()
var scrollDirection by remember { mutableStateOf(0f) }
val nestedScrollConnection = remember { object: NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
scrollDirection = available.y.sign
return Offset.Zero
}
} }
BackHandler { BackHandler {
when { when {
sheetState.isVisible -> coroutineScope.launch { sheetState.hide() } sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
@@ -492,7 +502,7 @@ class Manatoki(app: Application) : Source(), DIAware {
}, },
floatingActionButton = { floatingActionButton = {
AnimatedVisibility( AnimatedVisibility(
!model.fullscreen, !(model.fullscreen || scrollDirection < 0f),
enter = scaleIn(), enter = scaleIn(),
exit = scaleOut() exit = scaleOut()
) { ) {
@@ -562,7 +572,7 @@ class Manatoki(app: Application) : Source(), DIAware {
} }
) { contentPadding -> ) { contentPadding ->
ReaderBase( ReaderBase(
Modifier.padding(contentPadding), Modifier.padding(contentPadding).nestedScroll(nestedScrollConnection),
model model
) )
} }
@@ -711,7 +721,7 @@ class Manatoki(app: Application) : Source(), DIAware {
} }
) { contentPadding -> ) { contentPadding ->
Box(Modifier.padding(contentPadding)) { Box(Modifier.padding(contentPadding)) {
SearchOptionDrawer( ModalTopSheetLayout(
modifier = Modifier.run { modifier = Modifier.run {
if (drawerState.currentValue == ModalTopSheetState.Hidden) if (drawerState.currentValue == ModalTopSheetState.Hidden)
offset(0.dp, handleOffset) offset(0.dp, handleOffset)

View File

@@ -5,4 +5,26 @@ option java_multiple_files = true;
message Settings { message Settings {
optional string recent_source = 1; optional string recent_source = 1;
optional ReaderOptions mainReaderOption = 2;
optional ReaderOptions fullscreenReaderOption = 3;
}
message ReaderOptions {
enum Layout {
AUTO = 0;
SINGLE_PAGE = 1;
DOUBLE_PAGE = 2;
}
enum Orientation {
VERTICAL_DOWN = 0;
VERTICAL_UP = 1;
HORIZONTAL_RIGHT = 2;
HORIZONTAL_LEFT = 3;
}
optional Layout layout = 1;
optional Orientation orientation = 2;
optional bool snap = 3;
optional bool padding = 4;
} }