[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="name" value="MavenLocal" />
<option name="url" value="file:/$USER_HOME$/.m2/repository" /> <option name="url" value="file:/$USER_HOME$/.m2/repository" />
</remote-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> </component>
</project> </project>

View File

@@ -137,7 +137,7 @@ dependencies {
implementation("ru.noties.markwon:core:3.1.0") implementation("ru.noties.markwon:core:3.1.0")
implementation("xyz.quaver:documentfilex:0.7.1") 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") 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.border
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.*
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.AutoFixHigh
@@ -39,15 +38,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
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
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat 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.ReaderOptions
import xyz.quaver.pupil.proto.settingsDataStore 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.FileXImageSource
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
@@ -248,69 +253,23 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
} }
} }
@ExperimentalMaterialApi val ReaderOptions.Orientation.isVertical: Boolean
@ExperimentalFoundationApi 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 @Composable
fun ReaderBase( fun ReaderOptionsSheet(readerOptions: ReaderOptions, onOptionsChange: (ReaderOptions.Builder.() -> Unit) -> Unit) {
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 = {
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.h6) { CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.h6) {
Column(Modifier.padding(16.dp, 0.dp)) { Column(Modifier.padding(16.dp, 0.dp)) {
val layout = mainReaderOptions.layout val layout = readerOptions.layout
val snap = mainReaderOptions.snap val snap = readerOptions.snap
val orientation = mainReaderOptions.orientation val orientation = readerOptions.orientation
val padding = mainReaderOptions.padding val padding = readerOptions.padding
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -326,14 +285,8 @@ fun ReaderBase(
ReaderOptions.Layout.AUTO to Icons.Default.AutoFixHigh ReaderOptions.Layout.AUTO to Icons.Default.AutoFixHigh
).forEach { (option, icon) -> ).forEach { (option, icon) ->
IconButton(onClick = { IconButton(onClick = {
coroutineScope.launch { onOptionsChange {
context.settingsDataStore.updateData { setLayout(option)
it.toBuilder().setMainReaderOption(
it.mainReaderOption.toBuilder()
.setLayout(option)
.build()
).build()
}
} }
}) { }) {
Icon( Icon(
@@ -350,12 +303,8 @@ fun ReaderBase(
val infiniteTransition = rememberInfiniteTransition() val infiniteTransition = rememberInfiniteTransition()
val isVertical = val isReverse = orientation.isReverse
orientation == ReaderOptions.Orientation.VERTICAL_DOWN || val isVertical = orientation.isVertical
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 animationOrientation = if (isReverse) -1f else 1f
val animationSpacing by animateFloatAsState(if (padding) 48f else 32f) val animationSpacing by animateFloatAsState(if (padding) 48f else 32f)
@@ -381,14 +330,8 @@ fun ReaderBase(
else -> error("Invalid value") else -> error("Invalid value")
} }
coroutineScope.launch { onOptionsChange {
context.settingsDataStore.updateData { setOrientation(orientation)
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setOrientation(orientation)
.build()
).build()
}
} }
} }
@@ -447,14 +390,8 @@ fun ReaderBase(
Text("Snap") Text("Snap")
Switch(checked = snap, onCheckedChange = { Switch(checked = snap, onCheckedChange = {
coroutineScope.launch { onOptionsChange {
context.settingsDataStore.updateData { setSnap(!snap)
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setSnap(!snap)
.build()
).build()
}
} }
}) })
} }
@@ -467,14 +404,8 @@ fun ReaderBase(
Text("Padding") Text("Padding")
Switch(checked = padding, onCheckedChange = { Switch(checked = padding, onCheckedChange = {
coroutineScope.launch { onOptionsChange {
context.settingsDataStore.updateData { setPadding(!padding)
it.toBuilder().setMainReaderOption(
mainReaderOptions.toBuilder()
.setPadding(!padding)
.build()
).build()
}
} }
}) })
} }
@@ -485,27 +416,119 @@ fun ReaderBase(
.height(8.dp)) .height(8.dp))
} }
} }
}
@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( LazyColumn(
Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.align(Alignment.TopStart) .align(Alignment.TopStart)
.nestedScroll(nestedScrollConnection), .nestedScroll(nestedScrollConnection),
state = state,
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars) contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars),
) { reverseLayout = isReverse,
itemsIndexed(model.imageList) { i, uri -> content = content
val state = rememberSubSampledImageState(ScaleTypes.FIT_WIDTH) )
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( @Composable
Modifier fun ReaderLayoutItem(
.wrapContentHeight(state, 500.dp) 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() .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 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) if (progress == Float.NEGATIVE_INFINITY)
Icon(Icons.Filled.BrokenImage, null, tint = Orange500) Icon(Icons.Filled.BrokenImage, null, tint = Orange500)
@@ -514,7 +537,7 @@ fun ReaderBase(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
LinearProgressIndicator(progress) LinearProgressIndicator(progress)
Text((i + 1).toString()) Text((index + 1).toString())
} }
else if (uri != null && progress == Float.POSITIVE_INFINITY) { else if (uri != null && progress == Float.POSITIVE_INFINITY) {
val imageSource = kotlin.runCatching { val imageSource = kotlin.runCatching {
@@ -540,13 +563,171 @@ fun ReaderBase(
imageSource = imageSource, imageSource = imageSource,
state = state, state = state,
onError = { 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() }) if (model.progressList.any { it.isFinite() })
LinearProgressIndicator( LinearProgressIndicator(

View File

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