[Manatoki] Drawer highlight
This commit is contained in:
10
.idea/deploymentTargetDropDown.xml
generated
10
.idea/deploymentTargetDropDown.xml
generated
@@ -1,17 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="deploymentTargetDropDown">
|
<component name="deploymentTargetDropDown">
|
||||||
<runningDeviceTargetSelectedWithDropDown>
|
<targetSelectedWithDropDown>
|
||||||
<Target>
|
<Target>
|
||||||
<type value="RUNNING_DEVICE_TARGET" />
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
<deviceKey>
|
<deviceKey>
|
||||||
<Key>
|
<Key>
|
||||||
<type value="VIRTUAL_DEVICE_PATH" />
|
<type value="VIRTUAL_DEVICE_PATH" />
|
||||||
<value value="$USER_HOME$/.android/avd/Pixel_3a_API_30_x86.avd" />
|
<value value="$USER_HOME$/.android/avd/Pixel_2_API_30.avd" />
|
||||||
</Key>
|
</Key>
|
||||||
</deviceKey>
|
</deviceKey>
|
||||||
</Target>
|
</Target>
|
||||||
</runningDeviceTargetSelectedWithDropDown>
|
</targetSelectedWithDropDown>
|
||||||
<timeTargetWasSelectedWithDropDown value="2021-12-20T09:02:43.106748Z" />
|
<timeTargetWasSelectedWithDropDown value="2021-12-21T07:51:21.968371Z" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -31,6 +31,8 @@
|
|||||||
<entry key="../../../../layout/compose-model-1639625734547.xml" value="0.1" />
|
<entry key="../../../../layout/compose-model-1639625734547.xml" value="0.1" />
|
||||||
<entry key="../../../../layout/compose-model-1639629588722.xml" value="0.3472222222222222" />
|
<entry key="../../../../layout/compose-model-1639629588722.xml" value="0.3472222222222222" />
|
||||||
<entry key="../../../../layout/compose-model-1639809297022.xml" value="0.1" />
|
<entry key="../../../../layout/compose-model-1639809297022.xml" value="0.1" />
|
||||||
|
<entry key="../../../../layout/compose-model-1639997860317.xml" value="0.35555555555555557" />
|
||||||
|
<entry key="../../../../layout/compose-model-1639997910182.xml" value="0.35555555555555557" />
|
||||||
<entry key="../../../../layout/custom_preview.xml" value="0.518974358974359" />
|
<entry key="../../../../layout/custom_preview.xml" value="0.518974358974359" />
|
||||||
<entry key="app/src/main/res/drawable/avd_star.xml" value="0.2722222222222222" />
|
<entry key="app/src/main/res/drawable/avd_star.xml" value="0.2722222222222222" />
|
||||||
<entry key="app/src/main/res/drawable/close.xml" value="0.31614583333333335" />
|
<entry key="app/src/main/res/drawable/close.xml" value="0.31614583333333335" />
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ dependencies {
|
|||||||
implementation("androidx.compose.material:material-icons-extended:1.0.5")
|
implementation("androidx.compose.material:material-icons-extended:1.0.5")
|
||||||
implementation("androidx.compose.runtime:runtime-livedata:1.0.5")
|
implementation("androidx.compose.runtime:runtime-livedata:1.0.5")
|
||||||
implementation("androidx.compose.ui:ui-util:1.0.5")
|
implementation("androidx.compose.ui:ui-util:1.0.5")
|
||||||
|
implementation("androidx.compose.animation:animation:1.1.0-rc01")
|
||||||
implementation("androidx.activity:activity-compose:1.4.0")
|
implementation("androidx.activity:activity-compose:1.4.0")
|
||||||
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
|
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
tools:replace="android:theme"
|
tools:replace="android:theme"
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,7 @@ class Pupil : Application(), DIAware {
|
|||||||
}
|
}
|
||||||
install(HttpCache)
|
install(HttpCache)
|
||||||
|
|
||||||
install(UserAgent) {
|
BrowserUserAgent()
|
||||||
agent = "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"
|
|
||||||
}
|
|
||||||
//BrowserUserAgent()
|
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.sources.composable
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.gestures.forEachGesture
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
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.filled.NavigateBefore
|
||||||
|
import androidx.compose.material.icons.filled.NavigateNext
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
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.changedToUpIgnoreConsumed
|
||||||
|
import androidx.compose.ui.input.pointer.consumePositionChange
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastFirstOrNull
|
||||||
|
import com.google.accompanist.insets.LocalWindowInsets
|
||||||
|
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.ui.theme.LightBlue300
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OverscrollPager(
|
||||||
|
currentPage: Int,
|
||||||
|
prevPageAvailable: Boolean,
|
||||||
|
nextPageAvailable: Boolean,
|
||||||
|
onPageTurn: (Int) -> Unit,
|
||||||
|
prevPageTurnIndicatorOffset: Dp = 0.dp,
|
||||||
|
nextPageTurnIndicatorOffset: Dp = 0.dp,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() }
|
||||||
|
|
||||||
|
var overscroll: Float? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
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 prevPageTurnIndicatorOffsetPx = LocalDensity.current.run { prevPageTurnIndicatorOffset.toPx() }
|
||||||
|
val nextPageTurnIndicatorOffsetPx = LocalDensity.current.run { nextPageTurnIndicatorOffset.toPx() }
|
||||||
|
|
||||||
|
if (topCircleRadius != 0f || bottomCircleRadius != 0f)
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
drawCircle(
|
||||||
|
LightBlue300.copy(alpha = 0.6f),
|
||||||
|
center = Offset(this.center.x, prevPageTurnIndicatorOffsetPx),
|
||||||
|
radius = topCircleRadius
|
||||||
|
)
|
||||||
|
drawCircle(
|
||||||
|
LightBlue300.copy(alpha = 0.6f),
|
||||||
|
center = Offset(this.center.x, this.size.height-pageTurnIndicatorHeight-nextPageTurnIndicatorOffsetPx),
|
||||||
|
radius = bottomCircleRadius
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isOverscrollOverHeight = overscroll?.let { abs(it) >= pageTurnIndicatorHeight } == true
|
||||||
|
LaunchedEffect(isOverscrollOverHeight) {
|
||||||
|
if (isOverscrollOverHeight) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
overscroll?.let { overscroll ->
|
||||||
|
if (overscroll > 0f)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.offset(0.dp, prevPageTurnIndicatorOffset),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.NavigateBefore,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
Text(stringResource(R.string.main_move_to_page, currentPage - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overscroll < 0f)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.offset(0.dp, -nextPageTurnIndicatorOffset),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.main_move_to_page, currentPage + 1))
|
||||||
|
Icon(
|
||||||
|
Icons.Default.NavigateNext,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(
|
||||||
|
0.dp,
|
||||||
|
overscroll
|
||||||
|
?.coerceIn(-pageTurnIndicatorHeight, pageTurnIndicatorHeight)
|
||||||
|
?.let { overscroll -> LocalDensity.current.run { overscroll.toDp() } }
|
||||||
|
?: 0.dp)
|
||||||
|
.nestedScroll(object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
val overscrollSnapshot = overscroll
|
||||||
|
|
||||||
|
return if (overscrollSnapshot == null || overscrollSnapshot == 0f) {
|
||||||
|
Offset.Zero
|
||||||
|
} else {
|
||||||
|
val newOverscroll =
|
||||||
|
if (overscrollSnapshot > 0f && available.y < 0f)
|
||||||
|
max(overscrollSnapshot + available.y, 0f)
|
||||||
|
else if (overscrollSnapshot < 0f && available.y > 0f)
|
||||||
|
min(overscrollSnapshot + available.y, 0f)
|
||||||
|
else
|
||||||
|
overscrollSnapshot
|
||||||
|
|
||||||
|
Offset(0f, newOverscroll - overscrollSnapshot).also {
|
||||||
|
overscroll = newOverscroll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
if (
|
||||||
|
available.y == 0f ||
|
||||||
|
!prevPageAvailable && available.y > 0f ||
|
||||||
|
!nextPageAvailable && available.y < 0f
|
||||||
|
) return Offset.Zero
|
||||||
|
|
||||||
|
return overscroll?.let {
|
||||||
|
overscroll = it + available.y
|
||||||
|
Offset(0f, available.y)
|
||||||
|
} ?: Offset.Zero
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.pointerInput(currentPage) {
|
||||||
|
forEachGesture {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
var pointer = down.id
|
||||||
|
overscroll = 0f
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
val dragEvent =
|
||||||
|
event.changes.fastFirstOrNull { it.id == pointer }!!
|
||||||
|
|
||||||
|
if (dragEvent.changedToUpIgnoreConsumed()) {
|
||||||
|
val otherDown = event.changes.fastFirstOrNull { it.pressed }
|
||||||
|
if (otherDown == null) {
|
||||||
|
dragEvent.consumePositionChange()
|
||||||
|
overscroll?.let {
|
||||||
|
if (abs(it) > pageTurnIndicatorHeight)
|
||||||
|
onPageTurn(currentPage - it.sign.toInt())
|
||||||
|
}
|
||||||
|
overscroll = null
|
||||||
|
break
|
||||||
|
} else
|
||||||
|
pointer = otherDown.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
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.util.fastFirstOrNull
|
import androidx.compose.ui.util.fastFirstOrNull
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
@@ -101,7 +102,6 @@ fun <T> SearchBase(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val haptic = LocalHapticFeedback.current
|
|
||||||
|
|
||||||
var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
|
var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
|
||||||
|
|
||||||
@@ -119,13 +119,9 @@ fun <T> SearchBase(
|
|||||||
|
|
||||||
val statusBarsPaddingValues = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.statusBars)
|
val statusBarsPaddingValues = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.statusBars)
|
||||||
|
|
||||||
val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() }
|
|
||||||
|
|
||||||
val searchBarDefaultOffset = statusBarsPaddingValues.calculateTopPadding() + 64.dp
|
val searchBarDefaultOffset = statusBarsPaddingValues.calculateTopPadding() + 64.dp
|
||||||
val searchBarDefaultOffsetPx = LocalDensity.current.run { searchBarDefaultOffset.roundToPx() }
|
val searchBarDefaultOffsetPx = LocalDensity.current.run { searchBarDefaultOffset.roundToPx() }
|
||||||
|
|
||||||
var overscroll: Float? by remember { mutableStateOf(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(navigationIconProgress) {
|
LaunchedEffect(navigationIconProgress) {
|
||||||
navigationIcon.progress = navigationIconProgress
|
navigationIcon.progress = navigationIconProgress
|
||||||
}
|
}
|
||||||
@@ -144,76 +140,21 @@ fun <T> SearchBase(
|
|||||||
}
|
}
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Box(Modifier.padding(contentPadding)) {
|
Box(Modifier.padding(contentPadding)) {
|
||||||
val topCircleRadius by animateFloatAsState(if (overscroll?.let { it >= pageTurnIndicatorHeight } == true) 1000f else 0f)
|
OverscrollPager(
|
||||||
val bottomCircleRadius by animateFloatAsState(if (overscroll?.let { it <= -pageTurnIndicatorHeight } == true) 1000f else 0f)
|
currentPage = model.currentPage,
|
||||||
|
prevPageAvailable = model.prevPageAvailable,
|
||||||
if (topCircleRadius != 0f || bottomCircleRadius != 0f)
|
nextPageAvailable = model.nextPageAvailable,
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
onPageTurn = { model.currentPage = it },
|
||||||
drawCircle(
|
prevPageTurnIndicatorOffset = searchBarDefaultOffset,
|
||||||
LightBlue300.copy(alpha = 0.6f),
|
nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars).calculateBottomPadding()
|
||||||
center = Offset(this.center.x, searchBarDefaultOffsetPx.toFloat()),
|
) {
|
||||||
radius = topCircleRadius
|
Box(
|
||||||
)
|
Modifier
|
||||||
drawCircle(
|
.nestedScroll(object: NestedScrollConnection {
|
||||||
LightBlue300.copy(alpha = 0.6f),
|
override fun onPreScroll(
|
||||||
center = Offset(this.center.x, this.size.height-pageTurnIndicatorHeight),
|
available: Offset,
|
||||||
radius = bottomCircleRadius
|
source: NestedScrollSource
|
||||||
)
|
): Offset {
|
||||||
}
|
|
||||||
|
|
||||||
val isOverscrollOverHeight = overscroll?.let { abs(it) >= pageTurnIndicatorHeight } == true
|
|
||||||
LaunchedEffect(isOverscrollOverHeight) {
|
|
||||||
if (isOverscrollOverHeight) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
}
|
|
||||||
|
|
||||||
overscroll?.let { overscroll ->
|
|
||||||
if (overscroll > 0f)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopCenter)
|
|
||||||
.offset(0.dp, 64.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.NavigateBefore,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colors.secondary,
|
|
||||||
modifier = Modifier.size(48.dp)
|
|
||||||
)
|
|
||||||
Text(stringResource(R.string.main_move_to_page, model.currentPage-1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overscroll < 0f)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.main_move_to_page, model.currentPage+1))
|
|
||||||
Icon(
|
|
||||||
Icons.Default.NavigateNext,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colors.secondary,
|
|
||||||
modifier = Modifier.size(48.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.offset(
|
|
||||||
0.dp,
|
|
||||||
overscroll
|
|
||||||
?.coerceIn(-pageTurnIndicatorHeight, pageTurnIndicatorHeight)
|
|
||||||
?.let { overscroll -> LocalDensity.current.run { overscroll.toDp() } }
|
|
||||||
?: 0.dp)
|
|
||||||
.nestedScroll(object : NestedScrollConnection {
|
|
||||||
override fun onPreScroll(
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource
|
|
||||||
): Offset {
|
|
||||||
val overscrollSnapshot = overscroll
|
|
||||||
|
|
||||||
if (overscrollSnapshot == null || overscrollSnapshot == 0f) {
|
|
||||||
model.searchBarOffset =
|
model.searchBarOffset =
|
||||||
(model.searchBarOffset + available.y.roundToInt()).coerceIn(
|
(model.searchBarOffset + available.y.roundToInt()).coerceIn(
|
||||||
-searchBarDefaultOffsetPx,
|
-searchBarDefaultOffsetPx,
|
||||||
@@ -223,72 +164,14 @@ fun <T> SearchBase(
|
|||||||
model.isFabVisible = available.y > 0f
|
model.isFabVisible = available.y > 0f
|
||||||
|
|
||||||
return Offset.Zero
|
return Offset.Zero
|
||||||
} else {
|
|
||||||
val newOverscroll =
|
|
||||||
if (overscrollSnapshot > 0f && available.y < 0f)
|
|
||||||
max(overscrollSnapshot + available.y, 0f)
|
|
||||||
else if (overscrollSnapshot < 0f && available.y > 0f)
|
|
||||||
min(overscrollSnapshot + available.y, 0f)
|
|
||||||
else
|
|
||||||
overscrollSnapshot
|
|
||||||
|
|
||||||
return Offset(0f, newOverscroll - overscrollSnapshot).also {
|
|
||||||
overscroll = newOverscroll
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
) {
|
||||||
override fun onPostScroll(
|
content(PaddingValues(0.dp, searchBarDefaultOffset, 0.dp, rememberInsetsPaddingValues(
|
||||||
consumed: Offset,
|
insets = LocalWindowInsets.current.navigationBars
|
||||||
available: Offset,
|
).calculateBottomPadding()))
|
||||||
source: NestedScrollSource
|
|
||||||
): Offset {
|
|
||||||
if (
|
|
||||||
available.y == 0f ||
|
|
||||||
!model.prevPageAvailable && available.y > 0f ||
|
|
||||||
!model.nextPageAvailable && available.y < 0f
|
|
||||||
) return Offset.Zero
|
|
||||||
|
|
||||||
return overscroll?.let {
|
|
||||||
overscroll = it + available.y
|
|
||||||
Offset(0f, available.y)
|
|
||||||
} ?: Offset.Zero
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
forEachGesture {
|
|
||||||
awaitPointerEventScope {
|
|
||||||
val down = awaitFirstDown(requireUnconsumed = false)
|
|
||||||
var pointer = down.id
|
|
||||||
overscroll = 0f
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
val event = awaitPointerEvent()
|
|
||||||
val dragEvent =
|
|
||||||
event.changes.fastFirstOrNull { it.id == pointer }!!
|
|
||||||
|
|
||||||
if (dragEvent.changedToUpIgnoreConsumed()) {
|
|
||||||
val otherDown = event.changes.fastFirstOrNull { it.pressed }
|
|
||||||
if (otherDown == null) {
|
|
||||||
dragEvent.consumePositionChange()
|
|
||||||
overscroll?.let {
|
|
||||||
model.currentPage -= it.sign.toInt()
|
|
||||||
}
|
|
||||||
overscroll = null
|
|
||||||
break
|
|
||||||
} else
|
|
||||||
pointer = otherDown.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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))
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ import xyz.quaver.pupil.sources.Source
|
|||||||
import xyz.quaver.pupil.sources.SourceEntries
|
import xyz.quaver.pupil.sources.SourceEntries
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceSelectDialog(navController: NavController, currentSource: String, onDismissRequest: () -> Unit = { }) {
|
fun SourceSelectDialog(navController: NavController, currentSource: String? = null, onDismissRequest: () -> Unit = { }) {
|
||||||
SourceSelectDialog(currentSource = currentSource, onDismissRequest = onDismissRequest) {
|
SourceSelectDialog(currentSource = currentSource, onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
navController.navigate(it.name) {
|
navController.navigate(it.name) {
|
||||||
popUpTo(currentSource) { inclusive = true }
|
currentSource?.let { popUpTo(currentSource) { inclusive = true } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Sou
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceSelectDialog(currentSource: String, onDismissRequest: () -> Unit = { }, onSelected: (Source) -> Unit = { }) {
|
fun SourceSelectDialog(currentSource: String? = null, onDismissRequest: () -> Unit = { }, onSelected: (Source) -> Unit = { }) {
|
||||||
val sourceEntries: SourceEntries by rememberInstance()
|
val sourceEntries: SourceEntries by rememberInstance()
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismissRequest) {
|
Dialog(onDismissRequest = onDismissRequest) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -55,6 +56,7 @@ import org.kodein.log.LoggerFactory
|
|||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
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.settingsDataStore
|
||||||
import xyz.quaver.pupil.sources.Source
|
import xyz.quaver.pupil.sources.Source
|
||||||
import xyz.quaver.pupil.sources.composable.*
|
import xyz.quaver.pupil.sources.composable.*
|
||||||
import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
|
import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
|
||||||
@@ -96,6 +98,15 @@ class Hitomi(app: Application) : Source(), DIAware {
|
|||||||
bookmarks?.toSet() ?: emptySet()
|
bookmarks?.toSet() ?: emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
context.settingsDataStore.updateData {
|
||||||
|
it.toBuilder()
|
||||||
|
.setRecentSource(name)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var sourceSelectDialog by remember { mutableStateOf(false) }
|
var sourceSelectDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (sourceSelectDialog)
|
if (sourceSelectDialog)
|
||||||
|
|||||||
@@ -20,25 +20,41 @@ package xyz.quaver.pupil.sources.manatoki
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.gestures.animateScrollBy
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.PressInteraction
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.*
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
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.List
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material.icons.filled.Star
|
|
||||||
import androidx.compose.material.icons.filled.StarOutline
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
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.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
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.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -47,12 +63,15 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navigation
|
import androidx.navigation.navigation
|
||||||
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.google.accompanist.insets.LocalWindowInsets
|
import com.google.accompanist.insets.LocalWindowInsets
|
||||||
import com.google.accompanist.insets.navigationBarsPadding
|
import com.google.accompanist.insets.navigationBarsPadding
|
||||||
|
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||||
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||||
import com.google.accompanist.insets.ui.Scaffold
|
import com.google.accompanist.insets.ui.Scaffold
|
||||||
import com.google.accompanist.insets.ui.TopAppBar
|
import com.google.accompanist.insets.ui.TopAppBar
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@@ -64,16 +83,20 @@ import org.kodein.log.LoggerFactory
|
|||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
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.settingsDataStore
|
||||||
import xyz.quaver.pupil.sources.Source
|
import xyz.quaver.pupil.sources.Source
|
||||||
|
import xyz.quaver.pupil.sources.composable.OverscrollPager
|
||||||
import xyz.quaver.pupil.sources.composable.ReaderBase
|
import xyz.quaver.pupil.sources.composable.ReaderBase
|
||||||
import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel
|
import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel
|
||||||
import xyz.quaver.pupil.sources.composable.SourceSelectDialog
|
import xyz.quaver.pupil.sources.composable.SourceSelectDialog
|
||||||
import xyz.quaver.pupil.sources.manatoki.composable.BoardButton
|
import xyz.quaver.pupil.sources.manatoki.composable.*
|
||||||
import xyz.quaver.pupil.sources.manatoki.composable.MangaListingBottomSheet
|
import xyz.quaver.pupil.sources.manatoki.viewmodel.*
|
||||||
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
|
|
||||||
import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
|
|
||||||
import xyz.quaver.pupil.ui.theme.Orange500
|
import xyz.quaver.pupil.ui.theme.Orange500
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalAnimationApi::class)
|
||||||
class Manatoki(app: Application) : Source(), DIAware {
|
class Manatoki(app: Application) : Source(), DIAware {
|
||||||
override val di by closestDI(app)
|
override val di by closestDI(app)
|
||||||
|
|
||||||
@@ -91,10 +114,11 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
navigation(route = name, startDestination = "manatoki.net/") {
|
navigation(route = name, startDestination = "manatoki.net/") {
|
||||||
composable("manatoki.net/") { Main(navController) }
|
composable("manatoki.net/") { Main(navController) }
|
||||||
composable("manatoki.net/reader/{itemID}") { Reader(navController) }
|
composable("manatoki.net/reader/{itemID}") { Reader(navController) }
|
||||||
|
composable("manatoki.net/search") { Search(navController) }
|
||||||
|
composable("manatoki.net/recent") { Recent(navController) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Main(navController: NavController) {
|
fun Main(navController: NavController) {
|
||||||
val model: MainViewModel = viewModel()
|
val model: MainViewModel = viewModel()
|
||||||
@@ -108,6 +132,15 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
mangaListing = it
|
mangaListing = it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
context.settingsDataStore.updateData {
|
||||||
|
it.toBuilder()
|
||||||
|
.setRecentSource(name)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
|
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
readerInfoMutex.withLock {
|
readerInfoMutex.withLock {
|
||||||
@@ -151,7 +184,7 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text("박사장 게섯거라")
|
Text("마나토끼")
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { sourceSelectDialog = true }) {
|
IconButton(onClick = { sourceSelectDialog = true }) {
|
||||||
@@ -171,6 +204,19 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
applyBottom = false
|
applyBottom = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate("manatoki.net/search")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Search,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Box(Modifier.padding(contentPadding)) {
|
Box(Modifier.padding(contentPadding)) {
|
||||||
@@ -180,10 +226,23 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
"최신화",
|
modifier = Modifier.fillMaxWidth(),
|
||||||
style = MaterialTheme.typography.h5
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
)
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"최신화",
|
||||||
|
style = MaterialTheme.typography.h5
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(onClick = { navController.navigate("manatoki.net/recent") }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -192,7 +251,10 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
items(model.recentUpload) { item ->
|
items(model.recentUpload) { item ->
|
||||||
Thumbnail(item) {
|
Thumbnail(item,
|
||||||
|
Modifier
|
||||||
|
.width(180.dp)
|
||||||
|
.aspectRatio(6 / 7f)) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
mangaListing = null
|
mangaListing = null
|
||||||
sheetState.show()
|
sheetState.show()
|
||||||
@@ -227,7 +289,20 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("만화 목록", style = MaterialTheme.typography.h5)
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("만화 목록", style = MaterialTheme.typography.h5)
|
||||||
|
|
||||||
|
IconButton(onClick = { navController.navigate("manatoki.net/search") }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -235,7 +310,10 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
items(model.mangaList) { item ->
|
items(model.mangaList) { item ->
|
||||||
Thumbnail(item) {
|
Thumbnail(item,
|
||||||
|
Modifier
|
||||||
|
.width(180.dp)
|
||||||
|
.aspectRatio(6f / 7)) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
mangaListing = null
|
mangaListing = null
|
||||||
sheetState.show()
|
sheetState.show()
|
||||||
@@ -286,7 +364,9 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
item.title,
|
item.title,
|
||||||
modifier = Modifier.weight(1f).padding(0.dp, 4.dp),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(0.dp, 4.dp),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
@@ -307,7 +387,6 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Reader(navController: NavController) {
|
fun Reader(navController: NavController) {
|
||||||
val model: ReaderBaseViewModel = viewModel()
|
val model: ReaderBaseViewModel = viewModel()
|
||||||
@@ -320,12 +399,14 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
|
val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
|
||||||
var readerInfo: ReaderInfo? by rememberSaveable { mutableStateOf(null) }
|
var readerInfo: ReaderInfo? by rememberSaveable { mutableStateOf(null) }
|
||||||
|
|
||||||
LaunchedEffect(itemID) {
|
LaunchedEffect(Unit) {
|
||||||
if (itemID != null)
|
if (itemID != null)
|
||||||
readerInfoMutex.withLock {
|
readerInfoMutex.withLock {
|
||||||
readerInfoCache.get(itemID)?.let {
|
readerInfoCache.get(itemID)?.let {
|
||||||
readerInfo = it
|
readerInfo = it
|
||||||
model.load(it.urls)
|
model.load(it.urls) {
|
||||||
|
set("User-Agent", imageUserAgent)
|
||||||
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
model.error = true
|
model.error = true
|
||||||
}
|
}
|
||||||
@@ -336,6 +417,14 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
||||||
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
|
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
|
||||||
|
val mangaListingRippleInteractionSource = remember { mutableStateListOf<MutableInteractionSource>() }
|
||||||
|
val navigationBarsPadding = LocalDensity.current.run {
|
||||||
|
rememberInsetsPaddingValues(
|
||||||
|
LocalWindowInsets.current.navigationBars
|
||||||
|
).calculateBottomPadding().toPx()
|
||||||
|
}
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
when {
|
when {
|
||||||
@@ -345,11 +434,20 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mangaListingListSize: Size? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
ModalBottomSheetLayout(
|
ModalBottomSheetLayout(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
|
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
|
||||||
sheetContent = {
|
sheetContent = {
|
||||||
MangaListingBottomSheet(mangaListing) {
|
MangaListingBottomSheet(
|
||||||
|
mangaListing,
|
||||||
|
onListSize = {
|
||||||
|
mangaListingListSize = it
|
||||||
|
},
|
||||||
|
rippleInteractionSource = mangaListingRippleInteractionSource,
|
||||||
|
listState = listState
|
||||||
|
) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
client.getItem(
|
client.getItem(
|
||||||
it,
|
it,
|
||||||
@@ -359,7 +457,7 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
readerInfoCache.put(it.itemID, it)
|
readerInfoCache.put(it.itemID, it)
|
||||||
}
|
}
|
||||||
navController.navigate("manatoki.net/reader/${it.itemID}") {
|
navController.navigate("manatoki.net/reader/${it.itemID}") {
|
||||||
popUpTo("manatoki.net/reader/$itemID") { inclusive = true }
|
popUpTo("manatoki.net/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,27 +508,73 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
AnimatedVisibility(
|
||||||
modifier = Modifier.navigationBarsPadding(),
|
!model.fullscreen,
|
||||||
onClick = {
|
enter = scaleIn(),
|
||||||
readerInfo?.let {
|
exit = scaleOut()
|
||||||
coroutineScope.launch {
|
) {
|
||||||
sheetState.show()
|
FloatingActionButton(
|
||||||
}
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
|
onClick = {
|
||||||
|
readerInfo?.let {
|
||||||
|
coroutineScope.launch {
|
||||||
|
sheetState.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (mangaListing?.itemID != it.listingItemID)
|
||||||
|
client.getItem(it.listingItemID, onListing = {
|
||||||
|
mangaListing = it
|
||||||
|
|
||||||
|
mangaListingRippleInteractionSource.addAll(
|
||||||
|
List(max(it.entries.size - mangaListingRippleInteractionSource.size, 0)) {
|
||||||
|
MutableInteractionSource()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
while (listState.layoutInfo.totalItemsCount != it.entries.size) {
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetIndex = it.entries.indexOfFirst { it.itemID == itemID }
|
||||||
|
|
||||||
|
listState.animateScrollToItem(targetIndex)
|
||||||
|
|
||||||
|
mangaListingListSize?.let { sheetSize ->
|
||||||
|
val targetItem = listState.layoutInfo.visibleItemsInfo.first {
|
||||||
|
it.key == itemID
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetItem.offset == 0) {
|
||||||
|
listState.animateScrollBy(
|
||||||
|
-(sheetSize.height - navigationBarsPadding - targetItem.size)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(200)
|
||||||
|
|
||||||
|
with (mangaListingRippleInteractionSource[targetIndex]) {
|
||||||
|
val interaction = PressInteraction.Press(
|
||||||
|
Offset(sheetSize.width/2, targetItem.size/2f)
|
||||||
|
)
|
||||||
|
|
||||||
|
emit(interaction)
|
||||||
|
emit(PressInteraction.Release(interaction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
if (mangaListing?.itemID != it.listingItemID)
|
|
||||||
client.getItem(it.listingItemID, onListing = {
|
|
||||||
mangaListing = it
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.List,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.List,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
@@ -441,4 +585,275 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Recent(navController: NavController) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text("최신 업데이트")
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.navigateUp() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.NavigateBefore,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentPadding = rememberInsetsPaddingValues(
|
||||||
|
LocalWindowInsets.current.statusBars,
|
||||||
|
applyBottom = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { contentPadding ->
|
||||||
|
Box(Modifier.padding(contentPadding)) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Search(navController: NavController) {
|
||||||
|
val model: SearchViewModel = viewModel()
|
||||||
|
|
||||||
|
var searchFocused by remember { mutableStateOf(false) }
|
||||||
|
val handleOffset by animateDpAsState(if (searchFocused) 0.dp else (-36).dp)
|
||||||
|
|
||||||
|
val drawerState = rememberSwipeableState(SearchOptionDrawerStates.Hidden)
|
||||||
|
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
||||||
|
|
||||||
|
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
model.search()
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler {
|
||||||
|
when {
|
||||||
|
sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
|
||||||
|
drawerState.currentValue != SearchOptionDrawerStates.Hidden ->
|
||||||
|
coroutineScope.launch { drawerState.animateTo(SearchOptionDrawerStates.Hidden) }
|
||||||
|
else -> navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalBottomSheetLayout(
|
||||||
|
sheetState = sheetState,
|
||||||
|
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
|
||||||
|
sheetContent = {
|
||||||
|
MangaListingBottomSheet(mangaListing) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
client.getItem(it, onReader = {
|
||||||
|
launch {
|
||||||
|
readerInfoMutex.withLock {
|
||||||
|
readerInfoCache.put(it.itemID, it)
|
||||||
|
}
|
||||||
|
sheetState.snapTo(ModalBottomSheetValue.Hidden)
|
||||||
|
navController.navigate("manatoki.net/reader/${it.itemID}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures { focusManager.clearFocus() }
|
||||||
|
},
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
TextField(
|
||||||
|
model.stx,
|
||||||
|
modifier = Modifier
|
||||||
|
.onFocusChanged {
|
||||||
|
searchFocused = it.isFocused
|
||||||
|
},
|
||||||
|
onValueChange = { model.stx = it },
|
||||||
|
placeholder = { Text("제목") },
|
||||||
|
textStyle = MaterialTheme.typography.subtitle1,
|
||||||
|
singleLine = true,
|
||||||
|
trailingIcon = {
|
||||||
|
if (model.stx != "" && searchFocused)
|
||||||
|
IconButton(onClick = { model.stx = "" }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = contentColorFor(MaterialTheme.colors.primarySurface)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onSearch = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
coroutineScope.launch {
|
||||||
|
drawerState.animateTo(SearchOptionDrawerStates.Hidden)
|
||||||
|
}
|
||||||
|
coroutineScope.launch {
|
||||||
|
model.search()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
textColor = contentColorFor(MaterialTheme.colors.primarySurface),
|
||||||
|
placeholderColor = contentColorFor(MaterialTheme.colors.primarySurface).copy(alpha = 0.75f),
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
cursorColor = MaterialTheme.colors.secondary,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
errorIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.navigateUp() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.NavigateBefore,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentPadding = rememberInsetsPaddingValues(
|
||||||
|
LocalWindowInsets.current.statusBars,
|
||||||
|
applyBottom = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { contentPadding ->
|
||||||
|
Box(Modifier.padding(contentPadding)) {
|
||||||
|
SearchOptionDrawer(
|
||||||
|
modifier = Modifier.run {
|
||||||
|
if (drawerState.currentValue == SearchOptionDrawerStates.Hidden)
|
||||||
|
offset(0.dp, handleOffset)
|
||||||
|
else
|
||||||
|
navigationBarsWithImePadding()
|
||||||
|
},
|
||||||
|
drawerState = drawerState,
|
||||||
|
drawerContent = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp, 0.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("작가")
|
||||||
|
TextField(model.artist, onValueChange = { model.artist = it })
|
||||||
|
|
||||||
|
Text("발행")
|
||||||
|
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
|
||||||
|
Chip("전체", model.publish.isEmpty()) {
|
||||||
|
model.publish = ""
|
||||||
|
}
|
||||||
|
availablePublish.forEach {
|
||||||
|
Chip(it, model.publish == it) {
|
||||||
|
model.publish = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("초성")
|
||||||
|
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
|
||||||
|
Chip("전체", model.jaum.isEmpty()) {
|
||||||
|
model.jaum = ""
|
||||||
|
}
|
||||||
|
availableJaum.forEach {
|
||||||
|
Chip(it, model.jaum == it) {
|
||||||
|
model.jaum = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("장르")
|
||||||
|
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
|
||||||
|
Chip("전체", model.tag.isEmpty()) {
|
||||||
|
model.tag.clear()
|
||||||
|
}
|
||||||
|
availableTag.forEach {
|
||||||
|
Chip(it, model.tag.contains(it)) {
|
||||||
|
if (model.tag.contains(it))
|
||||||
|
model.tag.remove(it)
|
||||||
|
else
|
||||||
|
model.tag[it] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("정렬")
|
||||||
|
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
|
||||||
|
Chip("기본", model.sst.isEmpty()) {
|
||||||
|
model.sst = ""
|
||||||
|
}
|
||||||
|
availableSst.entries.forEach { (k, v) ->
|
||||||
|
Chip(v, model.sst == k) {
|
||||||
|
model.sst = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
OverscrollPager(
|
||||||
|
currentPage = model.page,
|
||||||
|
prevPageAvailable = model.page > 1,
|
||||||
|
nextPageAvailable = model.page < model.maxPage,
|
||||||
|
onPageTurn = {
|
||||||
|
model.page = it
|
||||||
|
coroutineScope.launch {
|
||||||
|
model.search(resetPage = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
GridCells.Adaptive(minSize = 200.dp),
|
||||||
|
contentPadding = rememberInsetsPaddingValues(
|
||||||
|
LocalWindowInsets.current.navigationBars
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
items(model.result) { item ->
|
||||||
|
Thumbnail(
|
||||||
|
Thumbnail(item.itemID, item.title, item.thumbnail),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(3f / 4)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
mangaListing = null
|
||||||
|
sheetState.show()
|
||||||
|
}
|
||||||
|
coroutineScope.launch {
|
||||||
|
client.getItem(it, onListing = {
|
||||||
|
mangaListing = it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.loading)
|
||||||
|
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,24 +18,35 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.sources.manatoki.composable
|
package xyz.quaver.pupil.sources.manatoki.composable
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.indication
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
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.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.SubcomposeLayout
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
import coil.compose.rememberImagePainter
|
import coil.compose.rememberImagePainter
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.google.accompanist.insets.LocalWindowInsets
|
import com.google.accompanist.insets.LocalWindowInsets
|
||||||
import com.google.accompanist.insets.navigationBarsPadding
|
import com.google.accompanist.insets.navigationBarsPadding
|
||||||
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import xyz.quaver.pupil.sources.manatoki.MangaListing
|
import xyz.quaver.pupil.sources.manatoki.MangaListing
|
||||||
|
|
||||||
private val FabSpacing = 8.dp
|
private val FabSpacing = 8.dp
|
||||||
@@ -44,7 +55,6 @@ private enum class MangaListingBottomSheetLayoutContent { Top, Bottom, Fab }
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaListingBottomSheetLayout(
|
fun MangaListingBottomSheetLayout(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
floatingActionButton: @Composable () -> Unit,
|
floatingActionButton: @Composable () -> Unit,
|
||||||
top: @Composable () -> Unit,
|
top: @Composable () -> Unit,
|
||||||
bottom: @Composable () -> Unit
|
bottom: @Composable () -> Unit
|
||||||
@@ -93,13 +103,30 @@ fun MangaListingBottomSheetLayout(
|
|||||||
@Composable
|
@Composable
|
||||||
fun MangaListingBottomSheet(
|
fun MangaListingBottomSheet(
|
||||||
mangaListing: MangaListing? = null,
|
mangaListing: MangaListing? = null,
|
||||||
onOpenItem: (String) -> Unit = { }
|
onListSize: (Size) -> Unit = { },
|
||||||
|
listState: LazyListState = rememberLazyListState(),
|
||||||
|
rippleInteractionSource: List<MutableInteractionSource> = emptyList(),
|
||||||
|
onOpenItem: (String) -> Unit = { },
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
rippleInteractionSource.forEach {
|
||||||
|
coroutineScope.launch {
|
||||||
|
it.interactions.collect {
|
||||||
|
Log.d("PUPILD", it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
if (mangaListing == null)
|
if (mangaListing == null)
|
||||||
CircularProgressIndicator(Modifier.navigationBarsPadding().padding(16.dp).align(Alignment.Center))
|
CircularProgressIndicator(
|
||||||
|
Modifier
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.Center))
|
||||||
else
|
else
|
||||||
MangaListingBottomSheetLayout(
|
MangaListingBottomSheetLayout(
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -157,7 +184,8 @@ fun MangaListingBottomSheet(
|
|||||||
) {
|
) {
|
||||||
mangaListing.tags.forEach {
|
mangaListing.tags.forEach {
|
||||||
Card(
|
Card(
|
||||||
elevation = 4.dp
|
elevation = 4.dp,
|
||||||
|
backgroundColor = Color.White
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
it,
|
it,
|
||||||
@@ -177,16 +205,27 @@ fun MangaListingBottomSheet(
|
|||||||
},
|
},
|
||||||
bottom = {
|
bottom = {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned {
|
||||||
|
onListSize(it.size.toSize())
|
||||||
|
},
|
||||||
|
state = listState,
|
||||||
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
||||||
) {
|
) {
|
||||||
items(mangaListing.entries) { entry ->
|
itemsIndexed(mangaListing.entries, key = { _, entry -> entry.itemID }) { index, entry ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
onOpenItem(entry.itemID)
|
onOpenItem(entry.itemID)
|
||||||
}
|
}
|
||||||
|
.run {
|
||||||
|
rippleInteractionSource
|
||||||
|
.getOrNull(index)
|
||||||
|
?.let {
|
||||||
|
indication(it, rememberRipple())
|
||||||
|
} ?: this
|
||||||
|
}
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.sources.manatoki.composable
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.animation.core.TweenSpec
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
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.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import xyz.quaver.pupil.sources.manatoki.composable.SearchOptionDrawerStates.Hidden
|
||||||
|
import xyz.quaver.pupil.sources.manatoki.composable.SearchOptionDrawerStates.Expanded
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class SearchOptionDrawerShape(
|
||||||
|
private val cornerRadius: Dp,
|
||||||
|
private val handleRadius: Dp
|
||||||
|
): Shape {
|
||||||
|
|
||||||
|
private fun drawDrawerPath(
|
||||||
|
size: Size,
|
||||||
|
cornerRadius: Float,
|
||||||
|
handleRadius: Float
|
||||||
|
) = Path().apply {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
lineTo(x = size.width, y = 0f)
|
||||||
|
|
||||||
|
lineTo(x = size.width, y = size.height - cornerRadius)
|
||||||
|
|
||||||
|
arcTo(
|
||||||
|
Rect(
|
||||||
|
left = size.width - 2*cornerRadius,
|
||||||
|
top = size.height - 2*cornerRadius,
|
||||||
|
right = size.width,
|
||||||
|
bottom = size.height
|
||||||
|
),
|
||||||
|
startAngleDegrees = 0f,
|
||||||
|
sweepAngleDegrees = 90f,
|
||||||
|
forceMoveTo = false
|
||||||
|
)
|
||||||
|
|
||||||
|
lineTo(x = size.width / 2 + handleRadius, y = size.height)
|
||||||
|
|
||||||
|
arcTo(
|
||||||
|
Rect(
|
||||||
|
left = size.width/2 - handleRadius,
|
||||||
|
top = size.height - handleRadius,
|
||||||
|
right = size.width/2 + handleRadius,
|
||||||
|
bottom = size.height + handleRadius
|
||||||
|
),
|
||||||
|
startAngleDegrees = 0f,
|
||||||
|
sweepAngleDegrees = 180f,
|
||||||
|
forceMoveTo = false
|
||||||
|
)
|
||||||
|
|
||||||
|
lineTo(x = cornerRadius, y = size.height)
|
||||||
|
|
||||||
|
arcTo(
|
||||||
|
Rect(
|
||||||
|
left = 0f,
|
||||||
|
top = size.height - 2*cornerRadius,
|
||||||
|
right = 2*cornerRadius,
|
||||||
|
bottom = size.height
|
||||||
|
),
|
||||||
|
startAngleDegrees = 90f,
|
||||||
|
sweepAngleDegrees = 90f,
|
||||||
|
forceMoveTo = false
|
||||||
|
)
|
||||||
|
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createOutline(
|
||||||
|
size: Size,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
density: Density
|
||||||
|
): Outline = Outline.Generic(
|
||||||
|
path = drawDrawerPath(
|
||||||
|
size,
|
||||||
|
density.run { cornerRadius.toPx() },
|
||||||
|
density.run { handleRadius.toPx() }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SearchOptionDrawerStates {
|
||||||
|
Hidden,
|
||||||
|
Expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Scrim(
|
||||||
|
color: Color,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
visible: Boolean
|
||||||
|
) {
|
||||||
|
if (color.isSpecified) {
|
||||||
|
val alpha by animateFloatAsState(
|
||||||
|
targetValue = if (visible) 1f else 0f,
|
||||||
|
animationSpec = TweenSpec()
|
||||||
|
)
|
||||||
|
val dismissModifier = if (visible) {
|
||||||
|
Modifier.pointerInput(onDismiss) { detectTapGestures { onDismiss() } }
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
Canvas(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.then(dismissModifier)
|
||||||
|
) {
|
||||||
|
drawRect(color = color, alpha = alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ExperimentalMaterialApi
|
||||||
|
fun SearchOptionDrawer(
|
||||||
|
drawerContent: @Composable ColumnScope.() -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
drawerCornerRadius: Dp = SearchOptionDrawerDefaults.CornerRadius,
|
||||||
|
drawerHandleRadius: Dp = SearchOptionDrawerDefaults.HandleRadius,
|
||||||
|
drawerState: SwipeableState<SearchOptionDrawerStates> = rememberSwipeableState(Hidden),
|
||||||
|
drawerElevation: Dp = SearchOptionDrawerDefaults.Elevation,
|
||||||
|
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
|
||||||
|
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
|
||||||
|
scrimColor: Color = SearchOptionDrawerDefaults.scrimColor,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object: NestedScrollConnection {
|
||||||
|
override fun onPreScroll(
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
val delta = available.y
|
||||||
|
return if (delta > 0 && source == NestedScrollSource.Drag)
|
||||||
|
Offset(0f, drawerState.performDrag(delta))
|
||||||
|
else
|
||||||
|
Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
return if (drawerState.offset.value < 0f && source == NestedScrollSource.Drag)
|
||||||
|
Offset(0f, drawerState.performDrag(available.y))
|
||||||
|
else
|
||||||
|
Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
val toFling = available.y
|
||||||
|
return if (toFling > 0 && drawerState.offset.value < 0f) {
|
||||||
|
available
|
||||||
|
} else Velocity.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
drawerState.performFling(available.y)
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxWithConstraints {
|
||||||
|
var sheetHeight by remember { mutableStateOf<Float?>(null) }
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
content()
|
||||||
|
Scrim(
|
||||||
|
color = scrimColor,
|
||||||
|
onDismiss = {
|
||||||
|
coroutineScope.launch { drawerState.animateTo(Hidden) }
|
||||||
|
},
|
||||||
|
visible = drawerState.targetValue != Hidden
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
|
.offset {
|
||||||
|
IntOffset(0, drawerState.offset.value.roundToInt())
|
||||||
|
}
|
||||||
|
.drawerSwipeable(drawerState, sheetHeight)
|
||||||
|
.onGloballyPositioned {
|
||||||
|
sheetHeight = it.size.height.toFloat()
|
||||||
|
},
|
||||||
|
shape = SearchOptionDrawerShape(drawerCornerRadius, drawerHandleRadius),
|
||||||
|
elevation = drawerElevation,
|
||||||
|
color = drawerBackgroundColor,
|
||||||
|
contentColor = drawerContentColor
|
||||||
|
) {
|
||||||
|
Column(content = drawerContent)
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.offset(0.dp, drawerHandleRadius)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(2*drawerHandleRadius, drawerHandleRadius)
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.pointerInput(drawerState) {
|
||||||
|
detectTapGestures {
|
||||||
|
coroutineScope.launch {
|
||||||
|
drawerState.animateTo(Expanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalMaterialApi
|
||||||
|
private fun Modifier.drawerSwipeable(
|
||||||
|
drawerState: SwipeableState<SearchOptionDrawerStates>,
|
||||||
|
sheetHeight: Float?
|
||||||
|
) = this.then(
|
||||||
|
if (sheetHeight != null) {
|
||||||
|
val anchors = mapOf(
|
||||||
|
-sheetHeight to Hidden,
|
||||||
|
0f to Expanded
|
||||||
|
)
|
||||||
|
|
||||||
|
Modifier.swipeable(
|
||||||
|
state = drawerState,
|
||||||
|
anchors = anchors,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
enabled = drawerState.currentValue != Hidden,
|
||||||
|
resistance = null
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
object SearchOptionDrawerDefaults {
|
||||||
|
val Elevation = 16.dp
|
||||||
|
val scrimColor: Color
|
||||||
|
@Composable
|
||||||
|
get() = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
val CornerRadius = 32.dp
|
||||||
|
val HandleRadius = 32.dp
|
||||||
|
}
|
||||||
@@ -47,18 +47,19 @@ data class Thumbnail(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Thumbnail(
|
fun Thumbnail(
|
||||||
thumbnail: Thumbnail,
|
thumbnail: Thumbnail,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClick: (String) -> Unit = { }
|
onClick: (String) -> Unit = { }
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
elevation = 8.dp,
|
elevation = 8.dp,
|
||||||
modifier = Modifier.clickable { onClick(thumbnail.itemID) }
|
modifier = modifier.clickable { onClick(thumbnail.itemID) }
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.width(IntrinsicSize.Min)
|
modifier = Modifier.width(IntrinsicSize.Min)
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
modifier = Modifier.size(180.dp, 210.dp),
|
modifier = Modifier.fillMaxSize(),
|
||||||
painter = rememberImagePainter(thumbnail.thumbnail),
|
painter = rememberImagePainter(thumbnail.thumbnail),
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,21 @@
|
|||||||
package xyz.quaver.pupil.sources.manatoki
|
package xyz.quaver.pupil.sources.manatoki
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.common.util.concurrent.RateLimiter
|
import com.google.common.util.concurrent.RateLimiter
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
@@ -26,6 +41,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
@@ -75,6 +91,19 @@ data class ReaderInfo(
|
|||||||
val listingItemID: String
|
val listingItemID: String
|
||||||
): Parcelable
|
): Parcelable
|
||||||
|
|
||||||
|
@ExperimentalMaterialApi
|
||||||
|
@Composable
|
||||||
|
fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) {
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
|
backgroundColor = if (selected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
elevation = 4.dp
|
||||||
|
) {
|
||||||
|
Text(text, modifier = Modifier.padding(4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun HttpClient.getItem(
|
suspend fun HttpClient.getItem(
|
||||||
itemID: String,
|
itemID: String,
|
||||||
onListing: (MangaListing) -> Unit = { },
|
onListing: (MangaListing) -> Unit = { },
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.sources.manatoki.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.closestDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import org.kodein.log.LoggerFactory
|
||||||
|
import org.kodein.log.newLogger
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@Serializable
|
||||||
|
data class SearchResult(
|
||||||
|
val itemID: String,
|
||||||
|
val title: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
val artist: String,
|
||||||
|
val type: String,
|
||||||
|
val lastUpdate: String
|
||||||
|
): Parcelable
|
||||||
|
|
||||||
|
val availablePublish = listOf(
|
||||||
|
"주간",
|
||||||
|
"격주",
|
||||||
|
"월간",
|
||||||
|
"단편",
|
||||||
|
"단행본",
|
||||||
|
"완결"
|
||||||
|
)
|
||||||
|
|
||||||
|
val availableJaum = listOf(
|
||||||
|
"ㄱ",
|
||||||
|
"ㄴ",
|
||||||
|
"ㄷ",
|
||||||
|
"ㄹ",
|
||||||
|
"ㅁ",
|
||||||
|
"ㅂ",
|
||||||
|
"ㅅ",
|
||||||
|
"ㅇ",
|
||||||
|
"ㅈ",
|
||||||
|
"ㅊ",
|
||||||
|
"ㅋ",
|
||||||
|
"ㅌ",
|
||||||
|
"ㅍ",
|
||||||
|
"ㅎ",
|
||||||
|
"0-9",
|
||||||
|
"a-z"
|
||||||
|
)
|
||||||
|
|
||||||
|
val availableTag = listOf(
|
||||||
|
"17",
|
||||||
|
"BL",
|
||||||
|
"SF",
|
||||||
|
"TS",
|
||||||
|
"개그",
|
||||||
|
"게임",
|
||||||
|
"도박",
|
||||||
|
"드라마",
|
||||||
|
"라노벨",
|
||||||
|
"러브코미디",
|
||||||
|
"먹방",
|
||||||
|
"백합",
|
||||||
|
"붕탁",
|
||||||
|
"순정",
|
||||||
|
"스릴러",
|
||||||
|
"스포츠",
|
||||||
|
"시대",
|
||||||
|
"애니화",
|
||||||
|
"액션",
|
||||||
|
"음악",
|
||||||
|
"이세계",
|
||||||
|
"일상",
|
||||||
|
"전생",
|
||||||
|
"추리",
|
||||||
|
"판타지",
|
||||||
|
"학원",
|
||||||
|
"호러"
|
||||||
|
)
|
||||||
|
|
||||||
|
val availableSst = mapOf(
|
||||||
|
"as_view" to "인기순",
|
||||||
|
"as_good" to "추천순",
|
||||||
|
"as_comment" to "댓글순",
|
||||||
|
"as_bookmark" to "북마크순"
|
||||||
|
)
|
||||||
|
|
||||||
|
class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||||
|
override val di by closestDI(app)
|
||||||
|
|
||||||
|
private val logger = newLogger(LoggerFactory.default)
|
||||||
|
|
||||||
|
private val client: HttpClient by instance()
|
||||||
|
|
||||||
|
// 발행
|
||||||
|
var publish by mutableStateOf("")
|
||||||
|
// 초성
|
||||||
|
var jaum by mutableStateOf("")
|
||||||
|
// 장르
|
||||||
|
val tag = mutableStateMapOf<String, String>()
|
||||||
|
// 정렬
|
||||||
|
var sst by mutableStateOf("")
|
||||||
|
// 오름/내림
|
||||||
|
var sod by mutableStateOf("")
|
||||||
|
// 제목
|
||||||
|
var stx by mutableStateOf("")
|
||||||
|
// 작가
|
||||||
|
var artist by mutableStateOf("")
|
||||||
|
|
||||||
|
var page by mutableStateOf(1)
|
||||||
|
var maxPage by mutableStateOf(0)
|
||||||
|
|
||||||
|
var loading by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var error by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
val result = mutableStateListOf<SearchResult>()
|
||||||
|
|
||||||
|
private var searchJob: Job? = null
|
||||||
|
suspend fun search(resetPage: Boolean = true) = coroutineScope {
|
||||||
|
searchJob?.cancelAndJoin()
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
result.clear()
|
||||||
|
if (resetPage) page = 1
|
||||||
|
|
||||||
|
searchJob = launch {
|
||||||
|
runCatching {
|
||||||
|
val urlBuilder = StringBuilder("https://manatoki116.net/comic")
|
||||||
|
|
||||||
|
if (page != 1) urlBuilder.append("/p$page")
|
||||||
|
|
||||||
|
val args = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (publish.isNotEmpty()) args.add("publish=$publish")
|
||||||
|
if (jaum.isNotEmpty()) args.add("jaum=$jaum")
|
||||||
|
if (tag.isNotEmpty()) args.add("tag=${tag.keys.joinToString(",")}")
|
||||||
|
if (sst.isNotEmpty()) args.add("sst=$sst")
|
||||||
|
if (stx.isNotEmpty()) args.add("stx=$stx")
|
||||||
|
if (artist.isNotEmpty()) args.add("artist=$artist")
|
||||||
|
|
||||||
|
if (args.isNotEmpty()) urlBuilder.append('?')
|
||||||
|
|
||||||
|
urlBuilder.append(args.joinToString("&"))
|
||||||
|
|
||||||
|
val doc = Jsoup.parse(client.get(urlBuilder.toString()))
|
||||||
|
|
||||||
|
maxPage = doc.getElementsByClass("pagination").first()!!.getElementsByTag("a").maxOf { it.text().toIntOrNull() ?: 0 }
|
||||||
|
|
||||||
|
doc.getElementsByClass("list-item").forEach {
|
||||||
|
val itemID =
|
||||||
|
it.selectFirst(".img-item > a")!!.attr("href").takeLastWhile { it != '/' }
|
||||||
|
val title = it.getElementsByClass("title").first()!!.text()
|
||||||
|
val thumbnail = it.getElementsByTag("img").first()!!.attr("src")
|
||||||
|
val artist = it.getElementsByClass("list-artist").first()!!.text()
|
||||||
|
val type = it.getElementsByClass("list-publish").first()!!.text()
|
||||||
|
val lastUpdate = it.getElementsByClass("list-date").first()!!.text()
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
result.add(
|
||||||
|
SearchResult(
|
||||||
|
itemID,
|
||||||
|
title,
|
||||||
|
thumbnail,
|
||||||
|
artist,
|
||||||
|
type,
|
||||||
|
lastUpdate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
loading = false
|
||||||
|
error = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,18 +26,24 @@ import androidx.compose.material.MaterialTheme
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.insets.ProvideWindowInsets
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
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.LoggerFactory
|
||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
|
import xyz.quaver.pupil.proto.settingsDataStore
|
||||||
import xyz.quaver.pupil.sources.SourceEntries
|
import xyz.quaver.pupil.sources.SourceEntries
|
||||||
|
import xyz.quaver.pupil.sources.composable.SourceSelectDialog
|
||||||
import xyz.quaver.pupil.ui.theme.PupilTheme
|
import xyz.quaver.pupil.ui.theme.PupilTheme
|
||||||
|
|
||||||
|
|
||||||
@@ -56,7 +62,7 @@ class MainActivity : ComponentActivity(), DIAware {
|
|||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
PupilTheme {
|
PupilTheme {
|
||||||
ProvideWindowInsets {
|
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
@@ -72,16 +78,31 @@ class MainActivity : ComponentActivity(), DIAware {
|
|||||||
NavHost(navController, startDestination = "main") {
|
NavHost(navController, startDestination = "main") {
|
||||||
composable("main") {
|
composable("main") {
|
||||||
var launched by rememberSaveable { mutableStateOf(false) }
|
var launched by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var sourceSelectDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (sourceSelectDialog)
|
||||||
|
SourceSelectDialog(navController, null)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (!launched) {
|
val recentSource = context.settingsDataStore.data.map { it.recentSource }.first()
|
||||||
val source = it.arguments?.getString("source") ?: "manatoki.net"
|
|
||||||
navController.navigate(source)
|
if (recentSource.isEmpty()) {
|
||||||
|
sourceSelectDialog = true
|
||||||
launched = true
|
launched = true
|
||||||
} else {
|
} else {
|
||||||
onBackPressed()
|
if (!launched) {
|
||||||
|
navController.navigate(recentSource)
|
||||||
|
launched = true
|
||||||
|
} else {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
composable("settings") {
|
||||||
|
|
||||||
}
|
}
|
||||||
sources.forEach {
|
sources.forEach {
|
||||||
it.second.run {
|
it.second.run {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package xyz.quaver.pupil.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.contentColorFor
|
||||||
import androidx.compose.material.darkColors
|
import androidx.compose.material.darkColors
|
||||||
import androidx.compose.material.lightColors
|
import androidx.compose.material.lightColors
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -29,6 +30,7 @@ private val DarkColorPalette = darkColors(
|
|||||||
primary = LightBlue300,
|
primary = LightBlue300,
|
||||||
primaryVariant = LightBlue700,
|
primaryVariant = LightBlue700,
|
||||||
secondary = Pink600,
|
secondary = Pink600,
|
||||||
|
onPrimary = Color.White,
|
||||||
onSecondary = Color.White
|
onSecondary = Color.White
|
||||||
)
|
)
|
||||||
private val LightColorPalette = lightColors(
|
private val LightColorPalette = lightColors(
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ class NetworkCache(context: Context) : DIAware {
|
|||||||
progressFlow.emit(Float.POSITIVE_INFINITY)
|
progressFlow.emit(Float.POSITIVE_INFINITY)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Log.d("PUPILD-NC", it.message.toString())
|
|
||||||
file.delete()
|
file.delete()
|
||||||
FirebaseCrashlytics.getInstance().recordException(it)
|
FirebaseCrashlytics.getInstance().recordException(it)
|
||||||
progressFlow.emit(Float.NEGATIVE_INFINITY)
|
progressFlow.emit(Float.NEGATIVE_INFINITY)
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ option java_package = "xyz.quaver.pupil.proto";
|
|||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
|
|
||||||
message Settings {
|
message Settings {
|
||||||
|
optional string recent_source = 1;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user