[Reader] WIP

This commit is contained in:
tom5079
2021-12-24 17:10:22 +09:00
parent 7e52a2e296
commit f78c66a9f4
4 changed files with 433 additions and 253 deletions

View File

@@ -41,5 +41,10 @@
<option name="name" value="MavenLocal" />
<option name="url" value="file:/$USER_HOME$/.m2/repository" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenLocal" />
<option name="name" value="MavenLocal" />
<option name="url" value="file:/$USER_HOME$/.m2/repository/" />
</remote-repository>
</component>
</project>

View File

@@ -137,7 +137,7 @@ dependencies {
implementation("ru.noties.markwon:core:3.1.0")
implementation("xyz.quaver:documentfilex:0.7.1")
implementation("xyz.quaver:subsampledimage:0.0.1-alpha13-SNAPSHOT")
implementation("xyz.quaver:subsampledimage:0.0.1-alpha14-SNAPSHOT-DEV01")
implementation("com.google.guava:guava:31.0.1-jre")

View File

@@ -25,8 +25,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoFixHigh
@@ -39,15 +38,20 @@ 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.geometry.Size
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.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
@@ -78,6 +82,7 @@ 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.util.FileXImageSource
import xyz.quaver.pupil.util.NetworkCache
import xyz.quaver.pupil.util.activity
import xyz.quaver.pupil.util.rememberFileXImageSource
@@ -248,69 +253,23 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
}
}
@ExperimentalMaterialApi
@ExperimentalFoundationApi
val ReaderOptions.Orientation.isVertical: Boolean
get() =
this == ReaderOptions.Orientation.VERTICAL_DOWN ||
this == ReaderOptions.Orientation.VERTICAL_UP
val ReaderOptions.Orientation.isReverse: Boolean
get() =
this == ReaderOptions.Orientation.VERTICAL_UP ||
this == ReaderOptions.Orientation.HORIZONTAL_LEFT
@Composable
fun ReaderBase(
modifier: Modifier = Modifier,
model: ReaderBaseViewModel
) {
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
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) {
context.activity?.window?.let { window ->
ViewCompat.getWindowInsetsController(window.decorView)?.let {
if (model.fullscreen) {
it.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
it.hide(WindowInsetsCompat.Type.systemBars())
} else
it.show(WindowInsetsCompat.Type.systemBars())
}
}
}
if (model.error)
stringResource(R.string.reader_failed_to_find_gallery).let {
snackbarCoroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(
it,
duration = SnackbarDuration.Indefinite
)
}
}
Box(modifier) {
ModalTopSheetLayout(
modifier = Modifier.offset(0.dp, handleOffset),
drawerContent = {
fun ReaderOptionsSheet(readerOptions: ReaderOptions, onOptionsChange: (ReaderOptions.Builder.() -> Unit) -> Unit) {
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
val layout = readerOptions.layout
val snap = readerOptions.snap
val orientation = readerOptions.orientation
val padding = readerOptions.padding
Row(
modifier = Modifier.fillMaxWidth(),
@@ -326,14 +285,8 @@ fun ReaderBase(
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()
}
onOptionsChange {
setLayout(option)
}
}) {
Icon(
@@ -350,12 +303,8 @@ fun ReaderBase(
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 isReverse = orientation.isReverse
val isVertical = orientation.isVertical
val animationOrientation = if (isReverse) -1f else 1f
val animationSpacing by animateFloatAsState(if (padding) 48f else 32f)
@@ -381,14 +330,8 @@ fun ReaderBase(
else -> error("Invalid value")
}
coroutineScope.launch {
context.settingsDataStore.updateData {
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setOrientation(orientation)
.build()
).build()
}
onOptionsChange {
setOrientation(orientation)
}
}
@@ -447,14 +390,8 @@ fun ReaderBase(
Text("Snap")
Switch(checked = snap, onCheckedChange = {
coroutineScope.launch {
context.settingsDataStore.updateData {
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setSnap(!snap)
.build()
).build()
}
onOptionsChange {
setSnap(!snap)
}
})
}
@@ -467,14 +404,8 @@ fun ReaderBase(
Text("Padding")
Switch(checked = padding, onCheckedChange = {
coroutineScope.launch {
context.settingsDataStore.updateData {
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setPadding(!padding)
.build()
).build()
}
onOptionsChange {
setPadding(!padding)
}
})
}
@@ -486,26 +417,118 @@ fun ReaderBase(
}
}
}
@Composable
fun BoxScope.ReaderLazyList(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
orientation: ReaderOptions.Orientation,
onScroll: (direction: Float) -> Unit,
content: LazyListScope.() -> Unit
) {
val isReverse = orientation.isReverse
val nestedScrollConnection = remember(orientation) { object: NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
onScroll(
when (orientation) {
ReaderOptions.Orientation.VERTICAL_DOWN -> available.y.sign
ReaderOptions.Orientation.VERTICAL_UP -> -(available.y.sign)
ReaderOptions.Orientation.HORIZONTAL_RIGHT -> available.x.sign
ReaderOptions.Orientation.HORIZONTAL_LEFT -> -(available.x.sign)
}
)
return Offset.Zero
}
} }
when (orientation) {
ReaderOptions.Orientation.VERTICAL_DOWN,
ReaderOptions.Orientation.VERTICAL_UP ->
LazyColumn(
Modifier
modifier = modifier
.fillMaxSize()
.align(Alignment.TopStart)
.nestedScroll(nestedScrollConnection),
state = state,
verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
) {
itemsIndexed(model.imageList) { i, uri ->
val state = rememberSubSampledImageState(ScaleTypes.FIT_WIDTH)
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars),
reverseLayout = isReverse,
content = content
)
ReaderOptions.Orientation.HORIZONTAL_RIGHT,
ReaderOptions.Orientation.HORIZONTAL_LEFT ->
LazyRow(
modifier = modifier
.fillMaxSize()
.align(Alignment.CenterStart)
.nestedScroll(nestedScrollConnection),
state = state,
horizontalArrangement = Arrangement.spacedBy(4.dp),
reverseLayout = isReverse,
content = content
)
}
}
Box(
Modifier
.wrapContentHeight(state, 500.dp)
@Composable
fun ReaderLayoutItem(
modifier: Modifier = Modifier,
isVertical: Boolean,
content: @Composable (Modifier) -> Unit
) {
if (isVertical)
Row(
modifier = Modifier.fillMaxWidth()
) {
content(modifier.weight(1f))
}
else
Column(
modifier = Modifier.fillMaxHeight()
) {
content(modifier.weight(1f))
}
}
@ExperimentalFoundationApi
@Composable
fun ReaderItem(
model: ReaderBaseViewModel,
readerOptions: ReaderOptions,
listSize: Size,
imageSources: List<ImageSource?>,
) {
val context = LocalContext.current
val state = rememberSubSampledImageState(
when {
readerOptions.padding -> ScaleTypes.CENTER_INSIDE
readerOptions.orientation.isVertical -> ScaleTypes.FIT_WIDTH
else -> ScaleTypes.FIT_HEIGHT
}
)
val listSizeDp = LocalDensity.current.run { listSize.width.toDp() to listSize.height.toDp() }
val modifier = when {
readerOptions.padding -> Modifier.size(listSizeDp.first, listSizeDp.second)
readerOptions.orientation.isVertical -> Modifier
.wrapContentHeight(state, listSizeDp.second)
.fillMaxWidth()
.border(1.dp, Color.Gray),
else -> Modifier
.wrapContentWidth(state, listSizeDp.first)
.fillMaxHeight()
}
ReaderLayoutItem(modifier, readerOptions.orientation.isVertical) { modifier ->
indices.forEach { index ->
Box(
modifier.border(1.dp, Color.Gray),
contentAlignment = Alignment.Center
) {
val progress = model.progressList.getOrNull(i) ?: 0f
val progress = model.progressList.getOrNull(index) ?: 0f
val uri = model.imageList.getOrNull(index)
if (progress == Float.NEGATIVE_INFINITY)
Icon(Icons.Filled.BrokenImage, null, tint = Orange500)
@@ -514,7 +537,7 @@ fun ReaderBase(
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(progress)
Text((i + 1).toString())
Text((index + 1).toString())
}
else if (uri != null && progress == Float.POSITIVE_INFINITY) {
val imageSource = kotlin.runCatching {
@@ -540,13 +563,171 @@ fun ReaderBase(
imageSource = imageSource,
state = state,
onError = {
model.error(i)
model.error(index)
}
)
}
}
}
}
}
@ExperimentalFoundationApi
fun LazyListScope.ReaderLazyListContent(
model: ReaderBaseViewModel,
listSize: Size,
imageSources: List<ImageSource?>,
imageSizes: List<Size?>,
readerOptions: ReaderOptions
) {
when {
readerOptions.layout == ReaderOptions.Layout.SINGLE_PAGE ->
items(imageSources) { source ->
ReaderItem(model, readerOptions, listSize, listOf(source))
}
readerOptions.layout == ReaderOptions.Layout.DOUBLE_PAGE ->
items(imageSources.size/2 + (imageSources.size and 0x1)) { i ->
ReaderItem(model, readerOptions, listSize, imageSources.subList(2*i, 2*i+2))
}
else ->
items(imageSources) { source ->
ReaderItem(model, readerOptions, listSize, listOf(source))
}
}
}
@ExperimentalMaterialApi
@ExperimentalFoundationApi
@Composable
fun ReaderBase(
modifier: Modifier = Modifier,
model: ReaderBaseViewModel,
onScroll: (direction: Float) -> Unit = { }
) {
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
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())
LaunchedEffect(scrollDirection) {
onScroll(scrollDirection)
}
LaunchedEffect(model.fullscreen) {
context.activity?.window?.let { window ->
ViewCompat.getWindowInsetsController(window.decorView)?.let {
if (model.fullscreen) {
it.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
it.hide(WindowInsetsCompat.Type.systemBars())
} else
it.show(WindowInsetsCompat.Type.systemBars())
}
}
}
if (model.error)
stringResource(R.string.reader_failed_to_find_gallery).let {
snackbarCoroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(
it,
duration = SnackbarDuration.Indefinite
)
}
}
Box(modifier) {
ModalTopSheetLayout(
modifier = Modifier.offset(0.dp, handleOffset),
drawerContent = {
ReaderOptionsSheet(mainReaderOptions) { readerOptionsBlock ->
coroutineScope.launch {
context.settingsDataStore.updateData {
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder().apply(readerOptionsBlock).build()
).build()
}
}
}
}
) {
var listSize: Size? by remember { mutableStateOf(null) }
val listState = rememberLazyListState()
val nestedScrollConnection = remember { object: NestedScrollConnection {
override suspend fun onPreFling(available: Velocity): Velocity {
return if (mainReaderOptions.snap) {
val velocity = when (mainReaderOptions.orientation) {
ReaderOptions.Orientation.VERTICAL_DOWN -> available.y
ReaderOptions.Orientation.VERTICAL_UP -> -(available.y)
ReaderOptions.Orientation.HORIZONTAL_RIGHT -> available.x
ReaderOptions.Orientation.HORIZONTAL_LEFT -> -(available.x)
}
val index = listState.firstVisibleItemIndex
coroutineScope.launch {
when {
velocity < 0f -> listState.animateScrollToItem(index+1)
else -> listState.animateScrollToItem(index)
}
}
available
} else Velocity.Zero
}
} }
val imageSources = remember { mutableStateListOf<ImageSource?>() }
val imageSizes = remember { mutableStateListOf<Size?>() }
LaunchedEffect(model.imageList.count { it != null }) {
if (imageSources.size != model.imageList.size)
imageSources.addAll(List (model.imageList.size-imageSources.size) { null })
if (imageSizes.size != model.imageList.size)
imageSizes.addAll(List (model.imageList.size-imageSources.size) { null })
coroutineScope.launch {
model.imageList.forEachIndexed { i, uri ->
if (imageSources[i] == null && uri != null)
imageSources[i] = FileXImageSource(FileX(context, uri))
if (imageSizes[i] == null)
imageSources[i]?.let {
imageSizes[i] = it.imageSize
}
}
}
}
ReaderLazyList(
Modifier
.onGloballyPositioned { listSize = it.size.toSize() }
.nestedScroll(nestedScrollConnection),
listState,
mainReaderOptions.orientation,
onScroll = { scrollDirection = it },
) {
ReaderLazyListContent(
model,
listSize ?: Size.Zero,
imageSources,
imageSizes,
mainReaderOptions
)
}
if (model.progressList.any { it.isFinite() })
LinearProgressIndicator(

View File

@@ -415,13 +415,6 @@ class Manatoki(app: Application) : Source(), DIAware {
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 {
when {
@@ -572,8 +565,9 @@ class Manatoki(app: Application) : Source(), DIAware {
}
) { contentPadding ->
ReaderBase(
Modifier.padding(contentPadding).nestedScroll(nestedScrollConnection),
model
Modifier.padding(contentPadding),
model = model,
onScroll = { scrollDirection = it }
)
}
}