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