Insets
Nested navigation Hitomi main actions
This commit is contained in:
2
.idea/deploymentTargetDropDown.xml
generated
2
.idea/deploymentTargetDropDown.xml
generated
@@ -12,6 +12,6 @@
|
|||||||
</deviceKey>
|
</deviceKey>
|
||||||
</Target>
|
</Target>
|
||||||
</runningDeviceTargetSelectedWithDropDown>
|
</runningDeviceTargetSelectedWithDropDown>
|
||||||
<timeTargetWasSelectedWithDropDown value="2021-12-18T14:48:54.587703Z" />
|
<timeTargetWasSelectedWithDropDown value="2021-12-19T03:31:58.153375Z" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
package xyz.quaver.pupil.sources
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
import org.kodein.di.*
|
import org.kodein.di.*
|
||||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||||
|
|
||||||
@@ -28,14 +28,7 @@ abstract class Source {
|
|||||||
abstract val name: String
|
abstract val name: String
|
||||||
abstract val iconResID: Int
|
abstract val iconResID: Int
|
||||||
|
|
||||||
@Composable
|
open fun NavGraphBuilder.navGraph(navController: NavController) { }
|
||||||
open fun MainScreen(navController: NavController) { }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
open fun Search(navController: NavController) { }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
open fun Reader(navController: NavController) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias SourceEntry = Pair<String, Source>
|
typealias SourceEntry = Pair<String, Source>
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@@ -80,7 +77,8 @@ fun FloatingSearchBar(
|
|||||||
elevation = 8.dp
|
elevation = 8.dp
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxSize().padding(16.dp, 0.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
navigationIcon()
|
navigationIcon()
|
||||||
@@ -91,14 +89,15 @@ fun FloatingSearchBar(
|
|||||||
.padding(16.dp, 0.dp)
|
.padding(16.dp, 0.dp)
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
if (it.isFocused) onTextFieldFocused()
|
if (it.isFocused) onTextFieldFocused()
|
||||||
else onTextFieldUnfocused()
|
else onTextFieldUnfocused()
|
||||||
|
|
||||||
isFocused = it.isFocused
|
isFocused = it.isFocused
|
||||||
},
|
},
|
||||||
value = query,
|
value = query,
|
||||||
onValueChange = onQueryChange,
|
onValueChange = onQueryChange,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
cursorBrush = SolidColor(MaterialTheme.colors.primary),
|
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colors.onSurface),
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onSearch = {
|
onSearch = {
|
||||||
@@ -129,11 +128,14 @@ fun FloatingSearchBar(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
Modifier.fillMaxHeight(),
|
||||||
content = actions
|
horizontalArrangement = Arrangement.End,
|
||||||
)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = actions
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> ListSearchResult(searchResults: List<T>, content: @Composable (T) -> Unit) {
|
fun <T> ListSearchResult(searchResults: List<T>, contentPadding: PaddingValues = PaddingValues(0.dp), content: @Composable (T) -> Unit) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
Modifier.fillMaxSize(),
|
Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(0.dp, 64.dp, 0.dp, 0.dp),
|
contentPadding = contentPadding,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(searchResults) { itemInfo ->
|
items(searchResults) { itemInfo ->
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ private class FloatingActionButtonItemProvider : PreviewParameterProvider<SubFab
|
|||||||
@Composable
|
@Composable
|
||||||
fun MultipleFloatingActionButton(
|
fun MultipleFloatingActionButton(
|
||||||
@PreviewParameter(provider = FloatingActionButtonItemProvider::class) items: List<SubFabItem>,
|
@PreviewParameter(provider = FloatingActionButtonItemProvider::class) items: List<SubFabItem>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
fabIcon: ImageVector = Icons.Default.Add,
|
fabIcon: ImageVector = Icons.Default.Add,
|
||||||
visible: Boolean = true,
|
visible: Boolean = true,
|
||||||
targetState: FloatingActionButtonState = FloatingActionButtonState.COLLAPSED,
|
targetState: FloatingActionButtonState = FloatingActionButtonState.COLLAPSED,
|
||||||
@@ -150,6 +151,7 @@ fun MultipleFloatingActionButton(
|
|||||||
if (!visible) onStateChanged?.invoke(FloatingActionButtonState.COLLAPSED)
|
if (!visible) onStateChanged?.invoke(FloatingActionButtonState.COLLAPSED)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.accompanist.insets.LocalWindowInsets
|
||||||
|
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||||
|
import com.google.accompanist.insets.ui.Scaffold
|
||||||
|
import com.google.accompanist.insets.ui.TopAppBar
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -172,6 +177,16 @@ fun ReaderBase(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
val useDarkIcons = MaterialTheme.colors.isLight
|
||||||
|
|
||||||
|
SideEffect {
|
||||||
|
systemUiController.setSystemBarsColor(
|
||||||
|
color = Color.Transparent,
|
||||||
|
darkIcons = useDarkIcons
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!model.isFullscreen)
|
if (!model.isFullscreen)
|
||||||
@@ -186,7 +201,11 @@ fun ReaderBase(
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
//TODO
|
//TODO
|
||||||
}
|
},
|
||||||
|
contentPadding = rememberInsetsPaddingValues(
|
||||||
|
LocalWindowInsets.current.statusBars,
|
||||||
|
applyBottom = false
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -208,8 +227,8 @@ fun ReaderBase(
|
|||||||
},
|
},
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
snackbarHost = { scaffoldState.snackbarHostState }
|
snackbarHost = { scaffoldState.snackbarHostState }
|
||||||
) {
|
) { contentPadding ->
|
||||||
Box {
|
Box(Modifier.padding(contentPadding)) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
Modifier.fillMaxSize(),
|
Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
|||||||
@@ -24,20 +24,18 @@ import androidx.compose.animation.core.animateFloat
|
|||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.updateTransition
|
import androidx.compose.animation.core.updateTransition
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.foundation.gestures.forEachGesture
|
import androidx.compose.foundation.gestures.forEachGesture
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
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.NavigateBefore
|
import androidx.compose.material.icons.filled.NavigateBefore
|
||||||
import androidx.compose.material.icons.filled.NavigateNext
|
import androidx.compose.material.icons.filled.NavigateNext
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
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.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
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
|
||||||
@@ -55,6 +53,12 @@ import androidx.compose.ui.util.fastFirstOrNull
|
|||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
|
import com.google.accompanist.insets.LocalWindowInsets
|
||||||
|
import com.google.accompanist.insets.navigationBarsPadding
|
||||||
|
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||||
|
import com.google.accompanist.insets.systemBarsPadding
|
||||||
|
import com.google.accompanist.insets.ui.Scaffold
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.ui.theme.LightBlue300
|
import xyz.quaver.pupil.ui.theme.LightBlue300
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
@@ -95,7 +99,7 @@ fun <T> SearchBase(
|
|||||||
fabSubMenu: List<SubFabItem> = emptyList(),
|
fabSubMenu: List<SubFabItem> = emptyList(),
|
||||||
actions: @Composable RowScope.() -> Unit = { },
|
actions: @Composable RowScope.() -> Unit = { },
|
||||||
onSearch: () -> Unit = { },
|
onSearch: () -> Unit = { },
|
||||||
content: @Composable BoxScope.() -> Unit
|
content: @Composable BoxScope.(contentPadding: PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
@@ -115,11 +119,25 @@ fun <T> SearchBase(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val systemBarsPaddingValues = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.systemBars)
|
||||||
|
|
||||||
val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() }
|
val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() }
|
||||||
val searchBarHeight = LocalDensity.current.run { 64.dp.roundToPx() }
|
|
||||||
|
val searchBarDefaultOffset = systemBarsPaddingValues.calculateTopPadding() + 64.dp
|
||||||
|
val searchBarDefaultOffsetPx = LocalDensity.current.run { searchBarDefaultOffset.roundToPx() }
|
||||||
|
|
||||||
var overscroll: Float? by remember { mutableStateOf(null) }
|
var overscroll: Float? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
val useDarkIcons = MaterialTheme.colors.isLight
|
||||||
|
|
||||||
|
SideEffect {
|
||||||
|
systemUiController.setSystemBarsColor(
|
||||||
|
color = Color.Transparent,
|
||||||
|
darkIcons = useDarkIcons
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(navigationIconProgress) {
|
LaunchedEffect(navigationIconProgress) {
|
||||||
navigationIcon.progress = navigationIconProgress
|
navigationIcon.progress = navigationIconProgress
|
||||||
}
|
}
|
||||||
@@ -127,6 +145,7 @@ fun <T> SearchBase(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
MultipleFloatingActionButton(
|
MultipleFloatingActionButton(
|
||||||
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
items = fabSubMenu,
|
items = fabSubMenu,
|
||||||
visible = model.isFabVisible,
|
visible = model.isFabVisible,
|
||||||
targetState = isFabExpanded,
|
targetState = isFabExpanded,
|
||||||
@@ -135,8 +154,8 @@ fun <T> SearchBase(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) { contentPadding ->
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.padding(contentPadding)) {
|
||||||
val topCircleRadius by animateFloatAsState(if (overscroll?.let { it >= pageTurnIndicatorHeight } == true) 1000f else 0f)
|
val topCircleRadius by animateFloatAsState(if (overscroll?.let { it >= pageTurnIndicatorHeight } == true) 1000f else 0f)
|
||||||
val bottomCircleRadius by animateFloatAsState(if (overscroll?.let { it <= -pageTurnIndicatorHeight } == true) 1000f else 0f)
|
val bottomCircleRadius by animateFloatAsState(if (overscroll?.let { it <= -pageTurnIndicatorHeight } == true) 1000f else 0f)
|
||||||
|
|
||||||
@@ -144,7 +163,7 @@ fun <T> SearchBase(
|
|||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
drawCircle(
|
drawCircle(
|
||||||
LightBlue300.copy(alpha = 0.6f),
|
LightBlue300.copy(alpha = 0.6f),
|
||||||
center = Offset(this.center.x, searchBarHeight.toFloat()),
|
center = Offset(this.center.x, searchBarDefaultOffsetPx.toFloat()),
|
||||||
radius = topCircleRadius
|
radius = topCircleRadius
|
||||||
)
|
)
|
||||||
drawCircle(
|
drawCircle(
|
||||||
@@ -195,7 +214,9 @@ fun <T> SearchBase(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(
|
.offset(
|
||||||
0.dp,
|
0.dp,
|
||||||
overscroll?.coerceIn(-pageTurnIndicatorHeight, pageTurnIndicatorHeight)?.let { overscroll -> LocalDensity.current.run { overscroll.toDp() } }
|
overscroll
|
||||||
|
?.coerceIn(-pageTurnIndicatorHeight, pageTurnIndicatorHeight)
|
||||||
|
?.let { overscroll -> LocalDensity.current.run { overscroll.toDp() } }
|
||||||
?: 0.dp)
|
?: 0.dp)
|
||||||
.nestedScroll(object : NestedScrollConnection {
|
.nestedScroll(object : NestedScrollConnection {
|
||||||
override fun onPreScroll(
|
override fun onPreScroll(
|
||||||
@@ -207,7 +228,7 @@ fun <T> SearchBase(
|
|||||||
if (overscrollSnapshot == null || overscrollSnapshot == 0f) {
|
if (overscrollSnapshot == null || overscrollSnapshot == 0f) {
|
||||||
model.searchBarOffset =
|
model.searchBarOffset =
|
||||||
(model.searchBarOffset + available.y.roundToInt()).coerceIn(
|
(model.searchBarOffset + available.y.roundToInt()).coerceIn(
|
||||||
-searchBarHeight,
|
-searchBarDefaultOffsetPx,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -275,29 +296,29 @@ fun <T> SearchBase(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content = content
|
content = {
|
||||||
|
this.content(
|
||||||
|
PaddingValues(0.dp, searchBarDefaultOffset, 0.dp, 0.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (model.loading)
|
if (model.loading)
|
||||||
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||||
|
|
||||||
FloatingSearchBar(
|
FloatingSearchBar(
|
||||||
modifier = Modifier.offset(0.dp, LocalDensity.current.run { model.searchBarOffset.toDp() }),
|
modifier = Modifier
|
||||||
|
.systemBarsPadding()
|
||||||
|
.offset(0.dp, LocalDensity.current.run { model.searchBarOffset.toDp() }),
|
||||||
query = model.query,
|
query = model.query,
|
||||||
onQueryChange = { model.query = it },
|
onQueryChange = { model.query = it },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
Icon(
|
IconButton(onClick = { focusManager.clearFocus() }) {
|
||||||
painter = rememberDrawablePainter(navigationIcon),
|
Icon(
|
||||||
contentDescription = null,
|
painter = rememberDrawablePainter(navigationIcon),
|
||||||
modifier = Modifier
|
contentDescription = null
|
||||||
.size(24.dp)
|
)
|
||||||
.clickable(
|
}
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = rememberRipple(bounded = false)
|
|
||||||
) {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
actions = actions,
|
actions = actions,
|
||||||
onTextFieldFocused = { navigationIconState = NavigationIconState.ARROW },
|
onTextFieldFocused = { navigationIconState = NavigationIconState.ARROW },
|
||||||
|
|||||||
@@ -19,14 +19,27 @@
|
|||||||
package xyz.quaver.pupil.sources.hitomi
|
package xyz.quaver.pupil.sources.hitomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Shuffle
|
import androidx.compose.material.icons.filled.Shuffle
|
||||||
|
import androidx.compose.material.icons.filled.Sort
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.navigation
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -44,6 +57,7 @@ import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
|
|||||||
import xyz.quaver.pupil.sources.hitomi.lib.getGalleryInfo
|
import xyz.quaver.pupil.sources.hitomi.lib.getGalleryInfo
|
||||||
import xyz.quaver.pupil.sources.hitomi.lib.getReferer
|
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.dialog.SourceSelectDialog
|
||||||
|
|
||||||
class Hitomi(app: Application) : Source(), DIAware {
|
class Hitomi(app: Application) : Source(), DIAware {
|
||||||
override val di by closestDI(app)
|
override val di by closestDI(app)
|
||||||
@@ -58,16 +72,15 @@ class Hitomi(app: Application) : Source(), DIAware {
|
|||||||
override val name: String = "hitomi.la"
|
override val name: String = "hitomi.la"
|
||||||
override val iconResID: Int = R.drawable.hitomi
|
override val iconResID: Int = R.drawable.hitomi
|
||||||
|
|
||||||
@Composable
|
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
||||||
override fun MainScreen(navController: NavController) {
|
navigation(startDestination = "search", route = name) {
|
||||||
navController.navigate("search/hitomi.la") {
|
composable("search") { Search(navController) }
|
||||||
launchSingleTop = true
|
composable("reader/{itemID}") { Reader(navController) }
|
||||||
navController.popBackStack()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Search(navController: NavController) {
|
fun Search(navController: NavController) {
|
||||||
val model: HitomiSearchResultViewModel = viewModel()
|
val model: HitomiSearchResultViewModel = viewModel()
|
||||||
val database: AppDatabase by rememberInstance()
|
val database: AppDatabase by rememberInstance()
|
||||||
val bookmarkDao = remember { database.bookmarkDao() }
|
val bookmarkDao = remember { database.bookmarkDao() }
|
||||||
@@ -78,7 +91,21 @@ class Hitomi(app: Application) : Source(), DIAware {
|
|||||||
bookmarks?.toSet() ?: emptySet()
|
bookmarks?.toSet() ?: emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(model.currentPage) {
|
var sourceSelectDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (sourceSelectDialog)
|
||||||
|
SourceSelectDialog(
|
||||||
|
currentSource = name,
|
||||||
|
onDismissRequest = { sourceSelectDialog = false }
|
||||||
|
) {
|
||||||
|
sourceSelectDialog = false
|
||||||
|
navController.navigate("main/${it.name}") {
|
||||||
|
launchSingleTop = true
|
||||||
|
popUpTo("main/{source}") { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(model.currentPage, model.sortByPopularity) {
|
||||||
model.search()
|
model.search()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,11 +126,58 @@ class Hitomi(app: Application) : Source(), DIAware {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
actions = {
|
actions = {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
IconButton(onClick = { sourceSelectDialog = true }) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.hitomi),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = { expanded = true }) {
|
||||||
|
Icon(Icons.Default.Sort, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = { navController.navigate("settings") }) {
|
||||||
|
Icon(Icons.Default.Settings, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onClick: (Boolean?) -> Unit = {
|
||||||
|
expanded = false
|
||||||
|
|
||||||
|
it?.let {
|
||||||
|
model.sortByPopularity = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded, onDismissRequest = { onClick(null) }) {
|
||||||
|
DropdownMenuItem(onClick = { onClick(false) }) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.main_menu_sort_newest))
|
||||||
|
RadioButton(selected = !model.sortByPopularity, onClick = { onClick(false) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
DropdownMenuItem(onClick = { onClick(true) }){
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.main_menu_sort_popular))
|
||||||
|
RadioButton(selected = model.sortByPopularity, onClick = { onClick(true) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSearch = { model.search() }
|
onSearch = { model.search() }
|
||||||
) {
|
) { contentPadding ->
|
||||||
ListSearchResult(model.searchResults) {
|
ListSearchResult(model.searchResults, contentPadding = contentPadding) {
|
||||||
DetailedSearchResult(
|
DetailedSearchResult(
|
||||||
it,
|
it,
|
||||||
bookmarks = bookmarkSet,
|
bookmarks = bookmarkSet,
|
||||||
@@ -114,14 +188,14 @@ class Hitomi(app: Application) : Source(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { result ->
|
) { result ->
|
||||||
navController.navigate("reader/$name/${result.itemID}")
|
navController.navigate("reader/${result.itemID}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Reader(navController: NavController) {
|
fun Reader(navController: NavController) {
|
||||||
val model: ReaderBaseViewModel = viewModel()
|
val model: ReaderBaseViewModel = viewModel()
|
||||||
|
|
||||||
val database: AppDatabase by rememberInstance()
|
val database: AppDatabase by rememberInstance()
|
||||||
@@ -148,7 +222,6 @@ class Hitomi(app: Application) : Source(), DIAware {
|
|||||||
append("Referer", getReferer(galleryID))
|
append("Referer", getReferer(galleryID))
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
logger.warning(it)
|
|
||||||
model.error = true
|
model.error = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import kotlinx.coroutines.yield
|
|||||||
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.instance
|
import org.kodein.di.instance
|
||||||
|
import org.kodein.log.LoggerFactory
|
||||||
|
import org.kodein.log.newLogger
|
||||||
import xyz.quaver.pupil.db.AppDatabase
|
import xyz.quaver.pupil.db.AppDatabase
|
||||||
import xyz.quaver.pupil.sources.composable.SearchBaseViewModel
|
import xyz.quaver.pupil.sources.composable.SearchBaseViewModel
|
||||||
import xyz.quaver.pupil.sources.hitomi.lib.GalleryBlock
|
import xyz.quaver.pupil.sources.hitomi.lib.GalleryBlock
|
||||||
@@ -41,6 +43,8 @@ import kotlin.math.ceil
|
|||||||
class HitomiSearchResultViewModel(app: Application) : SearchBaseViewModel<HitomiSearchResult>(app), DIAware {
|
class HitomiSearchResultViewModel(app: Application) : SearchBaseViewModel<HitomiSearchResult>(app), DIAware {
|
||||||
override val di by closestDI(app)
|
override val di by closestDI(app)
|
||||||
|
|
||||||
|
private val logger = newLogger(LoggerFactory.default)
|
||||||
|
|
||||||
private val client: HttpClient by instance()
|
private val client: HttpClient by instance()
|
||||||
|
|
||||||
private val database: AppDatabase by instance()
|
private val database: AppDatabase by instance()
|
||||||
@@ -80,9 +84,9 @@ class HitomiSearchResultViewModel(app: Application) : SearchBaseViewModel<Hitomi
|
|||||||
maxPage = ceil(result.size / resultsPerPage.toDouble()).toInt()
|
maxPage = ceil(result.size / resultsPerPage.toDouble()).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
yield()
|
|
||||||
|
|
||||||
cache.slice((currentPage-1)*resultsPerPage until currentPage*resultsPerPage).forEach { galleryID ->
|
cache.slice((currentPage-1)*resultsPerPage until currentPage*resultsPerPage).forEach { galleryID ->
|
||||||
|
yield()
|
||||||
|
loading = false
|
||||||
searchResults.add(transform(getGalleryBlock(client, galleryID)))
|
searchResults.add(transform(getGalleryBlock(client, galleryID)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,25 +21,24 @@ package xyz.quaver.pupil.ui
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
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.direct
|
import org.kodein.di.instance
|
||||||
import org.kodein.log.LoggerFactory
|
import org.kodein.log.LoggerFactory
|
||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
|
import xyz.quaver.pupil.sources.SourceEntries
|
||||||
import xyz.quaver.pupil.ui.theme.PupilTheme
|
import xyz.quaver.pupil.ui.theme.PupilTheme
|
||||||
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
|
|
||||||
import xyz.quaver.pupil.util.source
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity(), DIAware {
|
class MainActivity : ComponentActivity(), DIAware {
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
|
|
||||||
private val model: MainViewModel by viewModels()
|
private val sources: SourceEntries by instance()
|
||||||
|
|
||||||
private val logger = newLogger(LoggerFactory.default)
|
private val logger = newLogger(LoggerFactory.default)
|
||||||
|
|
||||||
@@ -47,24 +46,17 @@ class MainActivity : ComponentActivity(), DIAware {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
PupilTheme {
|
PupilTheme {
|
||||||
val navController = rememberNavController()
|
ProvideWindowInsets {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
NavHost(navController, startDestination = "main/{source}") {
|
NavHost(navController, startDestination = "hitomi.la") {
|
||||||
composable("main/{source}") {
|
sources.forEach {
|
||||||
direct.source(it.arguments?.getString("source") ?: "hitomi.la")
|
it.second.run { navGraph(navController) }
|
||||||
.MainScreen(navController)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
composable("search/{source}") {
|
|
||||||
direct.source(it.arguments?.getString("source") ?: "hitomi.la")
|
|
||||||
.Search(navController)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable("reader/{source}/{itemID}") {
|
|
||||||
direct.source(it.arguments?.getString("source") ?: "hitomi.la")
|
|
||||||
.Reader(navController)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2021 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import org.kodein.di.DIAware
|
|
||||||
import org.kodein.di.android.x.closestDI
|
|
||||||
import org.kodein.di.direct
|
|
||||||
import org.kodein.log.LoggerFactory
|
|
||||||
import org.kodein.log.newLogger
|
|
||||||
import xyz.quaver.pupil.sources.Source
|
|
||||||
import xyz.quaver.pupil.util.source
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|
||||||
override val di by closestDI()
|
|
||||||
|
|
||||||
private val logger = newLogger(LoggerFactory.default)
|
|
||||||
|
|
||||||
private val defaultSourceFactory: (String) -> Source = {
|
|
||||||
direct.source(it)
|
|
||||||
}
|
|
||||||
private var sourceFactory: (String) -> Source = defaultSourceFactory
|
|
||||||
var source by mutableStateOf(sourceFactory("hitomi.la"))
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user