diff --git a/.idea/misc.xml b/.idea/misc.xml
index 22f913d7..f01303dd 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -26,8 +26,11 @@
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c2f87842..2ca85270 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,8 +22,7 @@ android {
}
buildTypes {
getByName("debug") {
- isDebuggable = false
- isMinifyEnabled = true
+ isDebuggable = true
applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG"
@@ -77,10 +76,12 @@ dependencies {
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
- implementation("com.google.accompanist:accompanist-flowlayout:0.20.2")
- implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.2")
- implementation("com.google.accompanist:accompanist-insets:0.20.2")
- implementation("com.google.accompanist:accompanist-insets-ui:0.20.2")
+ implementation("com.google.accompanist:accompanist-flowlayout:0.20.3")
+ implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.3")
+ implementation("com.google.accompanist:accompanist-insets:0.20.3")
+ implementation("com.google.accompanist:accompanist-insets-ui:0.20.3")
+ implementation("com.google.accompanist:accompanist-drawablepainter:0.20.3")
+ implementation("com.google.accompanist:accompanist-systemuicontroller:0.20.3")
implementation("io.coil-kt:coil-compose:1.3.2")
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
index 78dcf908..1287a144 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
@@ -152,7 +152,7 @@ class Hitomi(app: Application) : Source(), DIAware {
var cachedSortMode: Int = -1
private val cache = mutableListOf()
- override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair, Int> {
+ override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair, Int> = coroutineScope { withContext(Dispatchers.IO) {
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
cachedQuery = null
cache.clear()
@@ -179,8 +179,8 @@ class Hitomi(app: Application) : Source(), DIAware {
channel.close()
}
- return Pair(channel, cache.size)
- }
+ Pair(channel, cache.size)
+ } }
override suspend fun suggestion(query: String) : List {
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
index 0fb49925..8e5dd421 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -23,22 +23,33 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.forEachGesture
-import androidx.compose.foundation.gestures.scrollable
+import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Icon
-import androidx.compose.material.Scaffold
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+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.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInteropFilter
@@ -47,6 +58,10 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny
+import androidx.core.graphics.drawable.toBitmap
+import androidx.core.view.WindowCompat
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -68,6 +83,11 @@ import xyz.quaver.pupil.ui.viewmodel.MainViewModel
import xyz.quaver.pupil.util.*
import kotlin.math.*
+private enum class NavigationIconState {
+ MENU,
+ ARROW
+}
+
class MainActivity : ComponentActivity(), DIAware {
override val di by closestDI()
@@ -75,37 +95,36 @@ class MainActivity : ComponentActivity(), DIAware {
private val logger = newLogger(LoggerFactory.default)
+ @OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
- val source: Source? by model.source.observeAsState(null)
- val loading: Boolean by model.loading.observeAsState(false)
-
- var query by remember { mutableStateOf("") }
-
- var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
-
- val lazyListState = rememberLazyListState()
-
- val searchBarHeight = LocalDensity.current.run { 56.dp.roundToPx() }
- var searchBarOffset by remember { mutableStateOf(0) }
-
- LaunchedEffect(lazyListState) {
- var lastOffset = 0
-
- snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
- .distinctUntilChanged()
- .collect { newOffset ->
- val dy = newOffset - lastOffset
- lastOffset = newOffset
-
- if (abs(dy) < searchBarHeight)
- searchBarOffset = (searchBarOffset-dy).coerceIn(-searchBarHeight, 0)
- }
- }
-
PupilTheme {
+ val source: Source? by model.source.observeAsState(null)
+
+ var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
+ var isFabVisible by remember { mutableStateOf(true) }
+
+ val searchBarHeight = LocalDensity.current.run { 56.dp.roundToPx() }
+ var searchBarOffset by remember { mutableStateOf(0) }
+
+ val navigationIcon = remember { DrawerArrowDrawable(this) }
+ var navigationIconState by remember { mutableStateOf(NavigationIconState.MENU) }
+ val navigationIconTransition = updateTransition(navigationIconState, label = "navigationIconTransition")
+ val navigationIconProgress by navigationIconTransition.animateFloat(
+ label = "navigationIconProgress"
+ ) { state ->
+ when (state) {
+ NavigationIconState.MENU -> 0f
+ NavigationIconState.ARROW -> 1f
+ }
+ }
+
+ LaunchedEffect(navigationIconProgress) {
+ navigationIcon.progress = navigationIconProgress
+ }
+
Scaffold(
floatingActionButton = {
MultipleFloatingActionButton(
@@ -127,6 +146,7 @@ class MainActivity : ComponentActivity(), DIAware {
stringResource(R.string.main_open_gallery_by_id)
),
),
+ visible = isFabVisible,
targetState = isFabExpanded,
onStateChanged = {
isFabExpanded = it
@@ -136,8 +156,24 @@ class MainActivity : ComponentActivity(), DIAware {
) {
Box(Modifier.fillMaxSize()) {
LazyColumn(
- Modifier.fillMaxSize(),
- state = lazyListState,
+ Modifier
+ .fillMaxSize()
+ .nestedScroll(object : NestedScrollConnection {
+ override fun onPreScroll(
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ searchBarOffset =
+ (searchBarOffset + available.y.roundToInt()).coerceIn(
+ -searchBarHeight,
+ 0
+ )
+
+ isFabVisible = available.y > 0f
+
+ return Offset.Zero
+ }
+ }),
contentPadding = PaddingValues(0.dp, 56.dp, 0.dp, 0.dp)
) {
items(model.searchResults, key = { it.itemID }) { itemInfo ->
@@ -159,25 +195,36 @@ class MainActivity : ComponentActivity(), DIAware {
}
}
- if (loading)
+ if (model.loading)
CircularProgressIndicator(Modifier.align(Alignment.Center))
FloatingSearchBar(
modifier = Modifier.offset(0.dp, LocalDensity.current.run { searchBarOffset.toDp() }),
- query = query,
- onQueryChange = { query = it },
+ query = model.query,
+ onQueryChange = { model.query = it },
+ navigationIcon = {
+ Icon(
+ painter = rememberDrawablePainter(navigationIcon),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp)
+ )
+ },
actions = {
Icon(
Icons.Default.Sort,
contentDescription = null,
- modifier = Modifier.size(24.dp)
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
)
Icon(
Icons.Default.Settings,
contentDescription = null,
- modifier = Modifier.size(24.dp)
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
)
- }
+ },
+ onTextFieldFocused = { navigationIconState = NavigationIconState.ARROW },
+ onTextFieldUnfocused = { navigationIconState = NavigationIconState.MENU; model.resetAndQuery() }
)
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt
index 6b6e9cc5..8fbad34b 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt
@@ -19,23 +19,31 @@
package xyz.quaver.pupil.ui.composable
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Card
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.Menu
-import androidx.compose.runtime.Composable
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import xyz.quaver.pupil.R
+import xyz.quaver.pupil.util.KeyboardManager
@Preview
@Composable
@@ -43,16 +51,26 @@ fun FloatingSearchBar(
modifier: Modifier = Modifier,
query: String = "",
onQueryChange: (String) -> Unit = { },
- navigationIcon: @Composable () -> Unit = {
- Icon(
- Icons.Default.Menu,
- modifier = Modifier.size(24.dp),
- contentDescription = null,
- tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
- )
- },
- actions: @Composable RowScope.() -> Unit = { }
+ navigationIcon: @Composable () -> Unit = { },
+ actions: @Composable RowScope.() -> Unit = { },
+ onTextFieldFocused: () -> Unit = { },
+ onTextFieldUnfocused: () -> Unit = { }
) {
+ val context = LocalContext.current
+ val focusManager = LocalFocusManager.current
+
+ var isFocused by remember { mutableStateOf(false) }
+
+ DisposableEffect(context) {
+ val keyboardManager = KeyboardManager(context)
+ keyboardManager.attachKeyboardDismissListener {
+ focusManager.clearFocus()
+ }
+ onDispose {
+ keyboardManager.release()
+ }
+ }
+
Card(
modifier = modifier
.fillMaxWidth()
@@ -68,19 +86,46 @@ fun FloatingSearchBar(
navigationIcon()
BasicTextField(
- modifier = Modifier.weight(1f).padding(16.dp, 0.dp),
+ modifier = Modifier
+ .weight(1f)
+ .padding(16.dp, 0.dp)
+ .onFocusChanged {
+ if (it.isFocused) onTextFieldFocused()
+ else onTextFieldUnfocused()
+
+ isFocused = it.isFocused
+ },
value = query,
onValueChange = onQueryChange,
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colors.primary),
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(
+ onSearch = {
+ focusManager.clearFocus()
+ }
+ ),
decorationBox = { innerTextField ->
- if (query.isEmpty())
- Text(
- stringResource(R.string.search_hint),
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
- )
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(Modifier.weight(1f)) {
+ if (query.isEmpty())
+ Text(
+ stringResource(R.string.search_hint),
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
+ )
+ innerTextField()
+ }
- innerTextField()
+ if (query.isNotEmpty() && isFocused)
+ Icon(
+ Icons.Default.Close,
+ contentDescription = null,
+ tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f),
+ modifier = Modifier.clickable { onQueryChange("") }
+ )
+ }
}
)
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt
index 761f6edf..58f36f27 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt
@@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.Modifier.Companion.any
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
@@ -28,6 +29,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastAll
enum class FloatingActionButtonState(private val isExpanded: Boolean) {
COLLAPSED(false), EXPANDED(true);
@@ -112,6 +114,7 @@ private class FloatingActionButtonItemProvider : PreviewParameterProvider,
fabIcon: ImageVector = Icons.Default.Add,
+ visible: Boolean = true,
targetState: FloatingActionButtonState = FloatingActionButtonState.COLLAPSED,
onStateChanged: ((FloatingActionButtonState) -> Unit)? = null
) {
@@ -131,10 +134,14 @@ fun MultipleFloatingActionButton(
}
}
+ if (!visible) onStateChanged?.invoke(FloatingActionButtonState.COLLAPSED)
+
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
+ var allCollapsed = true
+
items.forEachIndexed { index, item ->
val delay = when (targetState) {
FloatingActionButtonState.COLLAPSED -> index
@@ -207,12 +214,26 @@ fun MultipleFloatingActionButton(
) {
item.onClick?.invoke(it)
}
+
+ if (buttonScale != 0f) allCollapsed = false
}
- FloatingActionButton(onClick = {
- onStateChanged?.invoke(!targetState)
- }) {
- Icon(modifier = Modifier.rotate(rotation), imageVector = fabIcon, contentDescription = null)
+ val visibilityTransition = updateTransition(targetState = visible || !allCollapsed, label = "visible")
+
+ val scale by visibilityTransition.animateFloat(
+ label = "main FAB scale"
+ ) { state ->
+ if (state) 1f else 0f
}
+
+ if (scale > 0f)
+ FloatingActionButton(
+ modifier = Modifier.scale(scale),
+ onClick = {
+ onStateChanged?.invoke(!targetState)
+ }
+ ) {
+ Icon(modifier = Modifier.rotate(rotation), imageVector = fabIcon, contentDescription = null)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
index e460d0ba..886b93e1 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
@@ -20,13 +20,18 @@ package xyz.quaver.pupil.ui.viewmodel
import android.annotation.SuppressLint
import android.app.Application
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import kotlinx.coroutines.*
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.direct
import org.kodein.di.instance
+import org.kodein.log.LoggerFactory
+import org.kodein.log.newLogger
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.sources.*
import xyz.quaver.pupil.util.Preferences
@@ -39,15 +44,17 @@ import kotlin.random.Random
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
override val di by closestDI()
+ private val logger = newLogger(LoggerFactory.default)
+
val searchResults = mutableStateListOf()
- private val _loading = MutableLiveData(false)
- val loading = _loading as LiveData
+ var loading by mutableStateOf(false)
+ private set
private var queryJob: Job? = null
private var suggestionJob: Job? = null
- val query = MutableLiveData()
+ var query by mutableStateOf("")
private val queryStack = mutableListOf()
private val defaultSourceFactory: (String) -> Source = {
@@ -90,11 +97,11 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
sortModeIndex.value = 0
}
- setQueryAndSearch()
+ query = ""
+ resetAndQuery()
}
- fun setQueryAndSearch(query: String = "") {
- this.query.value = query
+ fun resetAndQuery() {
queryStack.add(query)
setPage(1)
@@ -126,29 +133,26 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
suggestionJob?.cancel()
queryJob?.cancel()
- _loading.value = true
+ loading = true
+ searchResults.clear()
queryJob = viewModelScope.launch {
- launch(Dispatchers.Default) {
- val channel = withContext(Dispatchers.IO) {
- val (channel, count) = source.search(
- query.value ?: "",
- (currentPage - 1) * perPage until currentPage * perPage,
- sortModeIndex
- )
+ val (channel, count) = source.search(
+ query,
+ (currentPage - 1) * perPage until currentPage * perPage,
+ sortModeIndex
+ )
- totalItems.postValue(count)
+ logger.info { count.toString() }
- channel
- }
+ totalItems.postValue(count)
- for (result in channel) {
- yield()
- searchResults.add(result)
- }
-
- _loading.postValue(false)
+ for (result in channel) {
+ yield()
+ searchResults.add(result)
}
+
+ loading = false
}
}
@@ -165,7 +169,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
viewModelScope.launch {
withContext(Dispatchers.IO) {
_source.value?.search(
- query.value + Preferences["default_query", ""],
+ query + Preferences["default_query", ""],
random .. random,
sortModeIndex.value!!
)?.first?.receive()
@@ -182,7 +186,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
@SuppressLint("NullSafeMutableLiveData")
_suggestions.value = withContext(Dispatchers.IO) {
kotlin.runCatching {
- _source.value!!.suggestion(query.value!!)
+ _source.value!!.suggestion(query)
}.getOrElse { emptyList() }
}
}
@@ -195,7 +199,8 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
if (queryStack.removeLastOrNull() == null || queryStack.isEmpty())
return false
- setQueryAndSearch(queryStack.removeLast())
+ query = queryStack.removeLast()
+ resetAndQuery()
return true
}
diff --git a/app/src/main/java/xyz/quaver/pupil/util/KeyboardManager.kt b/app/src/main/java/xyz/quaver/pupil/util/KeyboardManager.kt
new file mode 100644
index 00000000..fe00822d
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/util/KeyboardManager.kt
@@ -0,0 +1,67 @@
+/*
+ * 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 .
+ */
+
+package xyz.quaver.pupil.util
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewTreeObserver
+
+//https://stackoverflow.com/questions/68389802/how-to-clear-textfield-focus-when-closing-the-keyboard-and-prevent-two-back-pres
+class KeyboardManager(context: Context) {
+ private val activity = context as Activity
+ private var keyboardDismissListener: KeyboardDismissListener? = null
+
+ private abstract class KeyboardDismissListener(
+ private val rootView: View,
+ private val onKeyboardDismiss: () -> Unit
+ ) : ViewTreeObserver.OnGlobalLayoutListener {
+ private var isKeyboardClosed: Boolean = false
+ override fun onGlobalLayout() {
+ val r = Rect()
+ rootView.getWindowVisibleDisplayFrame(r)
+ val screenHeight = rootView.rootView.height
+ val keypadHeight = screenHeight - r.bottom
+ if (keypadHeight > screenHeight * 0.15) {
+ // 0.15 ratio is right enough to determine keypad height.
+ isKeyboardClosed = false
+ } else if (!isKeyboardClosed) {
+ isKeyboardClosed = true
+ onKeyboardDismiss.invoke()
+ }
+ }
+ }
+
+ fun attachKeyboardDismissListener(onKeyboardDismiss: () -> Unit) {
+ val rootView = activity.findViewById(android.R.id.content)
+ keyboardDismissListener = object : KeyboardDismissListener(rootView, onKeyboardDismiss) {}
+ keyboardDismissListener?.let {
+ rootView.viewTreeObserver.addOnGlobalLayoutListener(it)
+ }
+ }
+
+ fun release() {
+ val rootView = activity.findViewById(android.R.id.content)
+ keyboardDismissListener?.let {
+ rootView.viewTreeObserver.removeOnGlobalLayoutListener(it)
+ }
+ keyboardDismissListener = null
+ }
+}
diff --git a/app/src/main/java/xyz/quaver/pupil/util/misc.kt b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
index a2f9d887..72c8dd07 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
@@ -25,14 +25,18 @@ import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.view.MenuItem
import android.view.View
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.toAndroidRect
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.lifecycle.MutableLiveData
+import com.google.accompanist.insets.LocalWindowInsets
import kotlinx.serialization.json.*
import org.kodein.di.DIAware
import org.kodein.di.DirectDIAware